mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
848d77b1c2 | ||
|
|
e4e8f2edae | ||
|
|
ea9bdcc961 | ||
|
|
79ad39260e | ||
|
|
f4936316ab | ||
|
|
8879187e6a | ||
|
|
258b80a6a5 | ||
|
|
a108e6e97e | ||
|
|
dfef7eda3b | ||
|
|
4467898473 | ||
|
|
17d16b987f | ||
|
|
8e86daac71 | ||
|
|
856720da49 | ||
|
|
8f2c150d1e | ||
|
|
7d8b4c980a | ||
|
|
932756c7a0 | ||
|
|
538aac9a28 | ||
|
|
856bf8f5fb | ||
|
|
e1758ae2e2 | ||
|
|
61b3154461 | ||
|
|
fb9b30d144 | ||
|
|
b0df96b13f | ||
|
|
a469062a32 | ||
|
|
89d5d5c7db | ||
|
|
b8c2d6b05d | ||
|
|
b247864414 | ||
|
|
d3bcd87cfa | ||
|
|
82e5b64bad | ||
|
|
73e0271c23 | ||
|
|
a2dabee0e9 | ||
|
|
6a27c6d9f2 | ||
|
|
213ced0c7f | ||
|
|
5086c23d47 | ||
|
|
ee345a5206 | ||
|
|
f74cddc3b1 | ||
|
|
5b986b8b26 | ||
|
|
14887b9814 | ||
|
|
ecc40315b3 | ||
|
|
e7aed7fcf0 | ||
|
|
cd1aa948f9 | ||
|
|
82613d016a | ||
|
|
3a66be585f | ||
|
|
0a4e36ae09 | ||
|
|
92643539cf | ||
|
|
a1281d1331 | ||
|
|
074ca0ef8f | ||
|
|
464a9633dc | ||
|
|
fc2d91c5bb | ||
|
|
d68169bffb | ||
|
|
7efdb04e1e | ||
|
|
0155e122fd | ||
|
|
eb03f16a77 | ||
|
|
5ac39641ab | ||
|
|
8d1e48e400 | ||
|
|
0021ccb49f | ||
|
|
8590c7e5b8 | ||
|
|
8c5475f78f | ||
|
|
dfa116eb70 | ||
|
|
3a9fd3c074 | ||
|
|
5a92ef3c11 | ||
|
|
d3902f5c93 | ||
|
|
c886f887ae | ||
|
|
fc5089ac59 | ||
|
|
e3602f464b | ||
|
|
f3db6a339c | ||
|
|
c05195c045 | ||
|
|
af981fc719 | ||
|
|
088a264910 | ||
|
|
d7e80ad51b | ||
|
|
b53ddd401f | ||
|
|
e9122bca9d | ||
|
|
b61e8435d1 | ||
|
|
146afb6532 | ||
|
|
854e9d1378 | ||
|
|
689878ce32 | ||
|
|
d7ab177cc5 | ||
|
|
f4c6093c47 | ||
|
|
9fedfe3699 | ||
|
|
26f07246e1 | ||
|
|
3ae4b3c4de | ||
|
|
c8f9f16791 | ||
|
|
88f0738500 | ||
|
|
03c79d5f2f | ||
|
|
e7c3b7bcfe | ||
|
|
c8becca044 | ||
|
|
543a27271f | ||
|
|
a62aba83a0 | ||
|
|
53c6cf5f45 | ||
|
|
89842e20da | ||
|
|
ef793aecf3 | ||
|
|
51d51409d3 | ||
|
|
371b5eac45 | ||
|
|
5319bd13d5 | ||
|
|
e10d055453 | ||
|
|
716254e655 | ||
|
|
4c00b1683f | ||
|
|
37c9db09c6 | ||
|
|
653e2c9be4 | ||
|
|
a2a9613da1 | ||
|
|
e8d92d0d34 | ||
|
|
755b98a8c0 |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Feature Proposals
|
||||
url: https://github.com/pyscript/pyscript/discussions/new?category=proposals
|
||||
|
||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
## Description
|
||||
|
||||
<!--Please describe the changes in your pull request in few words here. -->
|
||||
|
||||
## 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 -->
|
||||
|
||||
## Checklist
|
||||
|
||||
<!-- Note: Only user-facing changes require a changelog entry. Internal-only API changes do not require a changelog entry. Changes in documentation do not require a changelog entry. -->
|
||||
|
||||
- [ ] All tests pass locally
|
||||
- [ ] I have updated `docs/changelog.md`
|
||||
- [ ] I have created documentation for this(if applicable)
|
||||
9
.github/workflows/build-unstable.yml
vendored
9
.github/workflows/build-unstable.yml
vendored
@@ -20,7 +20,7 @@ on:
|
||||
|
||||
jobs:
|
||||
BuildAndTest:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
defaults:
|
||||
run:
|
||||
working-directory: pyscriptjs
|
||||
@@ -67,6 +67,9 @@ jobs:
|
||||
- name: Integration Tests
|
||||
run: make test-integration-parallel
|
||||
|
||||
- name: Examples Tests
|
||||
run: make test-examples
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: pyscript
|
||||
@@ -82,7 +85,7 @@ jobs:
|
||||
path: pyscriptjs/test_results
|
||||
if-no-files-found: error
|
||||
eslint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
defaults:
|
||||
run:
|
||||
working-directory: pyscriptjs
|
||||
@@ -115,7 +118,7 @@ jobs:
|
||||
run: npx eslint src -c .eslintrc.js
|
||||
|
||||
Deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
needs: BuildAndTest
|
||||
if: github.ref == 'refs/heads/main' # Only deploy on merge into main
|
||||
permissions:
|
||||
|
||||
2
.github/workflows/docs-release.yml
vendored
2
.github/workflows/docs-release.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
22
.github/workflows/docs-review.yml
vendored
22
.github/workflows/docs-review.yml
vendored
@@ -19,7 +19,7 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'pyscript'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
@@ -51,23 +51,3 @@ jobs:
|
||||
with:
|
||||
name: pyscript-docs-review-${{ github.event.number }}
|
||||
path: docs/_build/html/
|
||||
|
||||
# Deploy to S3
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1.6.1
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||
|
||||
- name: Copy redirect file
|
||||
run: aws s3 cp --quiet ./docs/_build/html/_static/redirect.html s3://docs.pyscript.net/index.html
|
||||
|
||||
- name: Sync to S3
|
||||
run: aws s3 sync --quiet ./docs/_build/html/ s3://docs.pyscript.net/review/${{ github.event.number }}/
|
||||
|
||||
- name: Adding step summary
|
||||
run: |
|
||||
echo "### Review documentation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "As with any pull request, you can find the rendered documentation version for pull request ${{ github.event.number }} here:"
|
||||
echo "" >> $GITHUB_STEP_SUMMARY # this is a blank line
|
||||
echo "https://docs.pyscript.net/review/${{ github.event.number }}/" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
6
.github/workflows/docs-unstable.yml
vendored
6
.github/workflows/docs-unstable.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
@@ -49,6 +49,10 @@ jobs:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||
|
||||
# Sync will only copy changed files
|
||||
- name: Sync Error
|
||||
run: aws s3 cp --quiet ./docs/_static/s3_error.html s3://docs.pyscript.net/error.html
|
||||
|
||||
# Sync will only copy changed files
|
||||
- name: Sync to S3
|
||||
run: aws s3 sync --quiet ./docs/_build/html/ s3://docs.pyscript.net/unstable/
|
||||
|
||||
18
.github/workflows/prepare-release.yml
vendored
18
.github/workflows/prepare-release.yml
vendored
@@ -15,7 +15,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -44,8 +44,20 @@ jobs:
|
||||
- name: Setup Environment
|
||||
run: make setup
|
||||
|
||||
- name: Build and Test
|
||||
run: make test
|
||||
- name: Build
|
||||
run: make build
|
||||
|
||||
- name: TypeScript Tests
|
||||
run: make test-ts
|
||||
|
||||
- name: Python Tests
|
||||
run: make test-py
|
||||
|
||||
- name: Integration Tests
|
||||
run: make test-integration-parallel
|
||||
|
||||
- name: Examples Tests
|
||||
run: make test-examples
|
||||
|
||||
- name: Zip build folder
|
||||
run: zip -r -q ./build.zip ./build
|
||||
|
||||
18
.github/workflows/publish-release.yml
vendored
18
.github/workflows/publish-release.yml
vendored
@@ -14,7 +14,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
@@ -46,8 +46,20 @@ jobs:
|
||||
- name: Setup Environment
|
||||
run: make setup
|
||||
|
||||
- name: Build and Test
|
||||
run: make test
|
||||
- name: Build
|
||||
run: make build
|
||||
|
||||
- name: TypeScript Tests
|
||||
run: make test-ts
|
||||
|
||||
- name: Python Tests
|
||||
run: make test-py
|
||||
|
||||
- name: Integration Tests
|
||||
run: make test-integration-parallel
|
||||
|
||||
- name: Examples Tests
|
||||
run: make test-examples
|
||||
|
||||
# Upload to S3
|
||||
- name: Configure AWS credentials
|
||||
|
||||
2
.github/workflows/publish-snapshot.yml
vendored
2
.github/workflows/publish-snapshot.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
|
||||
jobs:
|
||||
snapshot:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
2
.github/workflows/sync-examples.yml
vendored
2
.github/workflows/sync-examples.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
128
.github/workflows/test-next.yml
vendored
Normal file
128
.github/workflows/test-next.yml
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
name: "[CI] Test Next"
|
||||
|
||||
on:
|
||||
push: # Only run on merges into main that modify files under pyscriptjs/ and examples/
|
||||
branches:
|
||||
- next
|
||||
paths:
|
||||
- pyscript.core/**
|
||||
- pyscriptjs/**
|
||||
- examples/**
|
||||
- .github/workflows/test-next.yml # Test that workflow works when changed
|
||||
|
||||
pull_request: # Run on any PR that modifies files under pyscriptjs/ and examples/
|
||||
branches:
|
||||
- next
|
||||
paths:
|
||||
- pyscript.core/**
|
||||
- pyscriptjs/**
|
||||
- examples/**
|
||||
- .github/workflows/test-next.yml # Test that workflow works when changed
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
BuildAndTest:
|
||||
runs-on: ubuntu-latest-8core
|
||||
defaults:
|
||||
run:
|
||||
working-directory: pyscriptjs
|
||||
env:
|
||||
MINICONDA_PYTHON_VERSION: py38
|
||||
MINICONDA_VERSION: 4.11.0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: setup Miniconda
|
||||
uses: conda-incubator/setup-miniconda@v2
|
||||
|
||||
- name: Setup Environment
|
||||
run: make setup
|
||||
|
||||
- name: Build
|
||||
run: make build
|
||||
|
||||
- name: TypeScript Tests (core)
|
||||
run: make test-ts
|
||||
|
||||
- name: Python Tests
|
||||
run: make test-py
|
||||
|
||||
- name: install next deps
|
||||
working-directory: pyscript.core
|
||||
run: npm i
|
||||
|
||||
- name: Run next tests
|
||||
working-directory: pyscript.core
|
||||
run: npm test
|
||||
|
||||
- name: Integration Tests
|
||||
run: make test-integration-parallel
|
||||
|
||||
- name: Examples Tests
|
||||
run: make test-examples
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: pyscript
|
||||
path: |
|
||||
pyscriptjs/build/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: test_results
|
||||
path: pyscriptjs/test_results
|
||||
if-no-files-found: error
|
||||
eslint:
|
||||
runs-on: ubuntu-latest-8core
|
||||
defaults:
|
||||
run:
|
||||
working-directory: pyscriptjs
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: npm install
|
||||
run: npm install
|
||||
|
||||
- name: Eslint
|
||||
run: npx eslint src -c .eslintrc.js
|
||||
2
.github/workflows/test_report.yml
vendored
2
.github/workflows/test_report.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- completed
|
||||
jobs:
|
||||
report:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-8core
|
||||
steps:
|
||||
- uses: dorny/test-reporter@v1.6.0
|
||||
with:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Check out the docs at: https://pre-commit.com/
|
||||
ci:
|
||||
skip: [eslint]
|
||||
autoupdate_schedule: monthly
|
||||
|
||||
default_stages: [commit]
|
||||
repos:
|
||||
@@ -23,7 +24,7 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.254
|
||||
rev: v0.0.257
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
@@ -34,7 +35,7 @@ repos:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.2
|
||||
rev: v2.2.4
|
||||
hooks:
|
||||
- id: codespell # See 'pyproject.toml' for args
|
||||
additional_dependencies:
|
||||
@@ -47,13 +48,13 @@ repos:
|
||||
args: [--tab-width, "4"]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v8.35.0
|
||||
rev: v8.36.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
files: pyscriptjs/src/.*\.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx
|
||||
types: [file]
|
||||
additional_dependencies:
|
||||
- eslint@8.25.0
|
||||
- typescript@4.8.4
|
||||
- "@typescript-eslint/eslint-plugin@5.39.0"
|
||||
- "@typescript-eslint/parser@5.39.0"
|
||||
- typescript@5.0.4
|
||||
- "@typescript-eslint/eslint-plugin@5.58.0"
|
||||
- "@typescript-eslint/parser@5.58.0"
|
||||
|
||||
@@ -2,3 +2,4 @@ ISSUE_TEMPLATE
|
||||
*.min.*
|
||||
package-lock.json
|
||||
docs
|
||||
examples/panel.html
|
||||
|
||||
@@ -4,14 +4,21 @@ Thank you for wanting to contribute to the PyScript project!
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Contributing to PyScript](#contributing-to-pyscript)
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Contributing](#contributing)
|
||||
- [Reporting bugs](#reporting-bugs)
|
||||
- [Creating useful issues](#creating-useful-issues)
|
||||
- [Reporting security issues](#reporting-security-issues)
|
||||
- [Asking questions](#asking-questions)
|
||||
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
|
||||
- [Developing](#developing)
|
||||
- [Rebasing changes](#rebasing-changes)
|
||||
- [Building the docs](#building-the-docs)
|
||||
- [Places to start](#places-to-start)
|
||||
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
|
||||
- [Submitting a change](#submitting-a-change)
|
||||
- [License terms for contributions](#license-terms-for-contributions)
|
||||
- [Becoming a maintainer](#becoming-a-maintainer)
|
||||
- [Trademarks](#trademarks)
|
||||
@@ -43,7 +50,7 @@ If you have questions about the project, using PyScript, or anything else, pleas
|
||||
|
||||
## Places to start
|
||||
|
||||
If you would like to contribute to PyScript, but you aren't sure where to begin, here are some suggestions.
|
||||
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!
|
||||
|
||||
@@ -9,13 +9,14 @@ This document lists the Maintainers of the Project. Maintainers may be added onc
|
||||
| Philipp Rudiger | Anaconda, Inc |
|
||||
| Peter Wang | Anaconda, Inc |
|
||||
| Kevin Goldsmith | Anaconda, Inc |
|
||||
| Mariana Meireles | Anaconda, Inc |
|
||||
| Mariana Meireles | |
|
||||
| Nicholas H.Tollervey | Anaconda, Inc |
|
||||
| Madhur Tandon | Anaconda, Inc |
|
||||
| Ted Patrick | Anaconda, Inc |
|
||||
| Jeff Glass | --- |
|
||||
| Paul Everitt | --- |
|
||||
| Fabio Rosado | --- |
|
||||
| Jeff Glass | |
|
||||
| Paul Everitt | |
|
||||
| Fabio Rosado | Anaconda, Inc |
|
||||
| Andrea Giammarchi | Anaconda, Inc |
|
||||
|
||||
---
|
||||
|
||||
|
||||
4
docs/_static/examples/what-is-pyscript.html
vendored
4
docs/_static/examples/what-is-pyscript.html
vendored
@@ -3,6 +3,10 @@
|
||||
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
|
||||
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
|
||||
<style>
|
||||
h1 {
|
||||
color: #459db9;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
4
docs/_static/s3_error.html
vendored
Normal file
4
docs/_static/s3_error.html
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<html><head><meta http-equiv="refresh" content="5; URL='/latest/'" /></head><body>
|
||||
<h1>404 - File not found</h1>
|
||||
<p>You will be redirected to the latest documentation in 5 seconds.</p>
|
||||
</body></html>
|
||||
@@ -1,13 +1,69 @@
|
||||
# Release Notes
|
||||
|
||||
2023.01.1
|
||||
2023.XX.X
|
||||
=========
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- 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))
|
||||
- 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
|
||||
---------
|
||||
@@ -18,6 +74,7 @@ Bug fixes
|
||||
|
||||
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))
|
||||
|
||||
@@ -35,3 +92,4 @@ 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))
|
||||
|
||||
@@ -19,7 +19,7 @@ import os
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "PyScript"
|
||||
copyright = "(c) 2022, Anaconda, Inc."
|
||||
copyright = "(c) 2023, Anaconda, Inc."
|
||||
author = "Anaconda, Inc."
|
||||
language = "en"
|
||||
|
||||
|
||||
@@ -28,3 +28,11 @@ showWarning(`
|
||||
</p>
|
||||
`, "html")
|
||||
```
|
||||
|
||||
## Deprecation History
|
||||
|
||||
This section tracks deprecations of specific features, both for historical record and to help the development team remember to actually remove deprecated features in future releases.
|
||||
|
||||
|Attribute/Object/Functionality|Deprecated In|Removed In|
|
||||
|-|-|-|
|
||||
|`py-mount` attribute | (Release following 2023.03.1) | -|
|
||||
|
||||
@@ -41,7 +41,7 @@ git checkout -b <your branch name>
|
||||
* Run tests before pushing the changes
|
||||
|
||||
```
|
||||
make tests
|
||||
make test
|
||||
```
|
||||
|
||||
To learn more about tests please refer to the session [Quick guide to pytest](## Quick guide to pytest).
|
||||
@@ -146,8 +146,8 @@ $ pytest test_01_basic.py -k test_pyscript_hello -s
|
||||
[ 0.00 page.goto ] pyscript_hello.html
|
||||
[ 0.01 request ] 200 - fake_server - http://fake_server/pyscript_hello.html
|
||||
...
|
||||
[ 0.17 console.info ] [py-loader] Downloading pyodide-0.22.1...
|
||||
[ 0.18 request ] 200 - CACHED - https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.js
|
||||
[ 0.17 console.info ] [py-loader] Downloading pyodide-x.y.z...
|
||||
[ 0.18 request ] 200 - CACHED - https://cdn.jsdelivr.net/pyodide/vx.y.z/full/pyodide.js
|
||||
...
|
||||
[ 3.59 console.info ] [pyscript/main] PyScript page fully initialized
|
||||
[ 3.60 console.log ] hello pyscript
|
||||
@@ -190,6 +190,16 @@ $ pytest test_01_basic.py -k test_pyscript_hello -s --dev
|
||||
`--dev` implies `--headed --no-fake-server`. In addition, it also
|
||||
automatically open chrome dev tools.
|
||||
|
||||
#### To run only main thread or worker tests
|
||||
|
||||
By default, we run each test twice: one with `execution_thread = "main"` and
|
||||
one with `execution_thread = "worker"`. If you want to run only half of them,
|
||||
you can use `-m`:
|
||||
|
||||
```
|
||||
$ pytest -m main # run only the tests in the main thread
|
||||
$ pytest -m worker # ron only the tests in the web worker
|
||||
```
|
||||
|
||||
## Fake server, HTTP cache
|
||||
|
||||
@@ -207,25 +217,6 @@ If you want to temporarily disable the cache, the easiest thing is to use
|
||||
If you want to clear the cache, you can use the special option
|
||||
`--clear-http-cache`:
|
||||
|
||||
```
|
||||
$ pytest --clear-http-cache
|
||||
...
|
||||
-------------------- SmartRouter HTTP cache --------------------
|
||||
Requests found in the cache:
|
||||
https://raw.githubusercontent.com/pyscript/pyscript/main/README.md
|
||||
https://cdn.jsdelivr.net/pyodide/v0.22.1/full/repodata.json
|
||||
https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.asm.js
|
||||
https://cdn.jsdelivr.net/pyodide/v0.22.1/full/micropip-0.1-py3-none-any.whl
|
||||
https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.asm.data
|
||||
https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.js
|
||||
https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.asm.wasm
|
||||
https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide_py.tar
|
||||
https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyparsing-3.0.9-py3-none-any.whl
|
||||
https://cdn.jsdelivr.net/pyodide/v0.22.1/full/distutils.tar
|
||||
https://cdn.jsdelivr.net/pyodide/v0.22.1/full/packaging-21.3-py3-none-any.whl
|
||||
Cache cleared
|
||||
```
|
||||
|
||||
**NOTE**: this works only if you are inside `tests/integration`, or if you
|
||||
explicitly specify `tests/integration` from the command line. This is due to
|
||||
how `pytest` decides to search for and load the various `conftest.py`.
|
||||
|
||||
@@ -1,80 +1,240 @@
|
||||
# Setting up your development environment
|
||||
|
||||
* Fork the repository - [quicklink](https://github.com/pyscript/pyscript/fork)
|
||||
These steps will help you set up your development environment. We suggest completing each step before going to the next step, as some parts have dependencies on previous commands.
|
||||
|
||||
* Clone your fork of the project
|
||||
## Prepare your repository
|
||||
|
||||
```
|
||||
* Create a fork of the [PyScript github repository](https://github.com/pyscript/pyscript/fork) to your github.
|
||||
|
||||
* In your development machine, clone your fork of PyScript. Use this command in your terminal.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/<your username>/pyscript
|
||||
```
|
||||
|
||||
* Add the original project as your upstream (this will allow you to pull the latest changes)
|
||||
* With the following command, add the original project as your upstream. This will allow you to pull the latest changes.
|
||||
|
||||
```sh
|
||||
git remote add upstream https://github.com/pyscript/pyscript.git
|
||||
git pull upstream main
|
||||
```
|
||||
|
||||
* If the above fails, try this alternative:
|
||||
|
||||
```sh
|
||||
git remote remove upstream
|
||||
git remote add upstream git@github.com:pyscript/pyscript.git
|
||||
git pull upstream main
|
||||
```
|
||||
|
||||
* cd into the `pyscriptjs` folder using the line below in your terminal (if your terminal is already in pyscript then use **cd pyscriptjs** instead)
|
||||
## Install the dependencies
|
||||
|
||||
```
|
||||
* change directory into `pyscriptjs` using this command:
|
||||
|
||||
```sh
|
||||
cd pyscript/pyscriptjs
|
||||
```
|
||||
|
||||
* Install the dependencies with the command below (you must have `nodejs` >=16 and `make`)
|
||||
We need to ensure that we have installed `conda`, `nodejs` >= 16 and `make`, before we can continue.
|
||||
|
||||
* Install `conda` by downloading one of the following packages that include it [MiniConda](https://docs.conda.io/en/latest/miniconda.html) or [Anaconda](https://www.anaconda.com/download/).
|
||||
|
||||
* Install `nodejs` with at least version 16. This can be downloaded at [https://nodejs.org](https://nodejs.org)
|
||||
|
||||
* Ensure that `make` is available on your system:
|
||||
|
||||
* *Linux*. `make` is usually installed by default in most Linux distributions. In the case it is not installed, run the terminal command `sudo apt install make`.
|
||||
|
||||
* *OS X*. Install Apple Developer Tools. `make` is included in this package.
|
||||
|
||||
* *Windows*. It is recommended to use either Windows Subsystem for Linux (WSL) or GNUWin32 for installing `make`. Instructions can be found [in this StackOverflow question](https://stackoverflow.com/questions/32127524/how-to-install-and-use-make-in-windows).
|
||||
|
||||
* The following command will download and install the rest of the PyScript dependencies:
|
||||
|
||||
```
|
||||
make setup
|
||||
```
|
||||
**NOTE**: If `make setup` gives a node/npm version required error then go to [troubleshooting](https://github.com/pyscript/pyscript/blob/main/TROUBLESHOOTING.md)
|
||||
|
||||
* You can also run the examples locally by running the command below in your terminal
|
||||
* **NOTE**: If `make setup` gives an error on an incompatible version for node or npm, please refer to [troubleshooting](https://github.com/pyscript/pyscript/blob/main/TROUBLESHOOTING.md).
|
||||
|
||||
## Activating the environment
|
||||
|
||||
* After the above `make setup` command is completed, it will print out the command for activating the environment using the following format. Use this to work on the development environment:
|
||||
|
||||
```
|
||||
conda activate <environment name>
|
||||
```
|
||||
|
||||
## Deactivating the environment
|
||||
|
||||
* To deactivate the environment, use the following command:
|
||||
```
|
||||
conda deactivate
|
||||
```
|
||||
|
||||
|
||||
# Running PyScript examples server
|
||||
|
||||
The examples server is used to view and edit the example files.
|
||||
|
||||
* change directory into `pyscriptjs` using this command:
|
||||
|
||||
```sh
|
||||
cd pyscript/pyscriptjs
|
||||
```
|
||||
|
||||
* To build the examples, run this command:
|
||||
|
||||
```
|
||||
make examples
|
||||
```
|
||||
|
||||
* Run ***npm run dev*** to build and run the dev server. This will also watch for changes and rebuild when a file is saved.
|
||||
* To serve the examples, run this command:
|
||||
|
||||
```sh
|
||||
python -m http.server 8080 --directory examples
|
||||
```
|
||||
|
||||
* Alternately, you can also run this command if conda is not activated:
|
||||
|
||||
```sh
|
||||
conda run -p <environment name> python -m http.server 8080 --directory examples
|
||||
```
|
||||
|
||||
* You can access the examples server by visiting the following url in your browser: [http://localhost:8080](http://localhost:8080)
|
||||
|
||||
|
||||
# Running the PyScript development server
|
||||
|
||||
The PyScript development server will regularly check for any changes in the src directory. If any changes were detected, the server will rebuild itself to reflect the changes. This is useful for development with PyScript.
|
||||
|
||||
* change directory into `pyscriptjs` using this command:
|
||||
|
||||
```sh
|
||||
cd pyscript/pyscriptjs
|
||||
```
|
||||
|
||||
* Use the following command to build and run the PyScript dev server.
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
**NOTE**: To access your local build paste `http://localhost:8080` into your browser
|
||||
|
||||
* You can access the PyScript development server by visiting the following url in your browser: [http://localhost:8080](http://localhost:8080)
|
||||
|
||||
Now that node and npm have both been updated `make setup` should work, and you can continue [setting up your local environment](setting-up-environment.md) without problems (hopefully).
|
||||
# Setting up the test environment
|
||||
|
||||
A key to good development is to perform tests before sending a Pull Request for your changes.
|
||||
|
||||
## Setting up and building the docs
|
||||
## Install the dependencies
|
||||
|
||||
To build the documentation locally first make sure you are in the `docs` directory.
|
||||
* change directory into `pyscriptjs` using this command:
|
||||
|
||||
You'll need `make` and `conda` installed in your machine. The rest of the environment should be automatically download and created for you once you use the command:
|
||||
```sh
|
||||
cd pyscript/pyscriptjs
|
||||
```
|
||||
|
||||
* The following command will download the dependencies needed for running the tests:
|
||||
|
||||
```
|
||||
make setup
|
||||
```
|
||||
|
||||
Use `conda activate $environment_name` to activate your environment.
|
||||
* If you are not using a conda environment, or wish to install the dependencies manually, here are the packages needed:
|
||||
* `pillow`
|
||||
* `requests`
|
||||
* `numpy`
|
||||
* `playwright`
|
||||
* `pytest-playwright`. Note that this is only available as a `pip` package.
|
||||
|
||||
To add new information to the documentation make sure you conform with PyScript's code of conduct and with the general principles of Diataxis. Don't worry about reading too much on it, just do your best to keep your contributions on the correct axis.
|
||||
## Activating the environment
|
||||
|
||||
Write your documentation files using [Markedly Structured Text](https://myst-parser.readthedocs.io/en/latest/syntax/optional.html), which is very similar to vanilla Markdown but with some addons to create the documentation infrastructure.
|
||||
* After the above `make setup` command is completed, it will print out the command for activating the environment using the following format:
|
||||
|
||||
Once done, initialize a server to check your work:
|
||||
```
|
||||
conda activate <environment name>
|
||||
```
|
||||
|
||||
## Deactivating the environment
|
||||
|
||||
* To deactivate the environment, use the following command:
|
||||
```
|
||||
conda deactivate
|
||||
```
|
||||
|
||||
## Running the tests
|
||||
|
||||
* After setting up the test environment and while the environment is activated, you can run the tests with the following command:
|
||||
|
||||
```
|
||||
make test
|
||||
```
|
||||
|
||||
For more information about PyScript's testing framework, head over to the [development process](developing.md) page.
|
||||
|
||||
# Setting up your documentation environment
|
||||
|
||||
The documentation environment is separate from the development environment. It is used for updating and reviewing the documentation before deployment.
|
||||
|
||||
## Installing the dependencies
|
||||
|
||||
* change directory into the `docs` using this command:
|
||||
|
||||
```sh
|
||||
cd pyscript/docs
|
||||
```
|
||||
|
||||
* The following command will download, install the dependencies, and create the environment for you:
|
||||
|
||||
```
|
||||
make setup
|
||||
```
|
||||
|
||||
(activating-documentation-environment)=
|
||||
## Activating the environment
|
||||
|
||||
* After the above `make setup` command is completed, it will print out the command for activating the environment using the following format:
|
||||
|
||||
```
|
||||
conda activate <docs environment name>
|
||||
```
|
||||
|
||||
Note that the docs environment path is different from the developer's environment path.
|
||||
|
||||
## Deactivating the environment
|
||||
|
||||
* To deactivate the environment, use the following command:
|
||||
```
|
||||
conda deactivate
|
||||
```
|
||||
|
||||
## Contributing to the documentation
|
||||
|
||||
* Before sending a pull request, we recommend that your documentation conforms with [PyScript's code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md) and with the general principles of [Diataxis](https://diataxis.fr/). Don't worry about reading too much on it, just do your best to keep your contributions on the correct axis.
|
||||
|
||||
* Write your documentation files using [Markedly Structured Text](https://myst-parser.readthedocs.io/en/latest/syntax/optional.html). This is similar to Markdown but with some addons to create the documentation infrastructure.
|
||||
|
||||
## Reviewing your work
|
||||
|
||||
* Before sending a Pull Request, review your work by starting the documentation server. To do this, use the following command:
|
||||
|
||||
```
|
||||
make livehtml
|
||||
```
|
||||
|
||||
Visible here: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||
You can visit the documentation server by opening a browser and visiting [http://127.0.0.1:8000](http://127.0.0.1:8000).
|
||||
|
||||
## Setting up and building tests
|
||||
* Alternately, you can open a static documentation server. Unlike the above, this will not automatically update any changes done after running this server. To see the changes done, you will need to manually stop and restart the server. To do this, use the following command:
|
||||
|
||||
You'll need to install the following to have a functional test environment: `playwright`, `pytest-playwright`, `pillow`, `requests` and `numpy`.
|
||||
```
|
||||
make htmlserve
|
||||
```
|
||||
|
||||
`pytest-playwright`is only available as a `pip` package so we recommend that you install `playwright` and `pytest` from `pip`.
|
||||
You can visit the documentation server by opening a browser and visiting [http://127.0.0.1:8080](http://127.0.0.1:8080).
|
||||
|
||||
If you're interested to learn more about PyScript's testing framework, head over to the [development process](developing.md) page.
|
||||
* To stop either server, press `ctrl+C` or `command+C` while the shell running the command is active.
|
||||
|
||||
* Note: If the above make commands failed, you need to activate the documentation environment first before running any of the commands. Refer to [Activating the environment](#activating-documentation-environment) section above.
|
||||
|
||||
# PyScript Demonstrator
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ dependencies:
|
||||
- sphinx-copybutton
|
||||
- sphinx-design
|
||||
- sphinx-togglebutton
|
||||
- nodejs=16
|
||||
|
||||
- pip:
|
||||
- sphinxemoji
|
||||
|
||||
179
docs/guides/event-handlers.md
Normal file
179
docs/guides/event-handlers.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Event handlers in PyScript
|
||||
|
||||
PyScript offer two ways to subscribe to Javascript event handlers:
|
||||
|
||||
## Subscribe to event with `py-*` attributes
|
||||
|
||||
The value of the attribute contains python code which will be executed when the event is fired. A very common pattern is to call a function which does further work, for example:
|
||||
|
||||
```html
|
||||
<button id="noParam" py-click="say_hello_no_param()">
|
||||
No Event - No Params py-click
|
||||
</button>
|
||||
<button id="withParam" py-click="say_hello_with_param('World')">
|
||||
No Event - With Params py-click
|
||||
</button>
|
||||
```
|
||||
|
||||
```python
|
||||
<py-script>
|
||||
def say_hello_no_param():
|
||||
print("Hello!")
|
||||
|
||||
def say_hello_with_param(name):
|
||||
print("Hello " + name + "!")
|
||||
</py-script>
|
||||
```
|
||||
|
||||
Note that py-\* attributes need a _function call_
|
||||
|
||||
Supported py-\* attributes can be seen in the [PyScript API reference](<[../api-reference.md](https://github.com/pyscript/pyscript/blob/66b57bf812dcc472ed6ffee075ace5ced89bbc7c/pyscriptjs/src/components/pyscript.ts#L119-L260)>).
|
||||
|
||||
## Subscribe to event with `addEventListener`
|
||||
|
||||
You can also subscribe to an event using the `addEventListener` method of the DOM element. This is useful if you want to pass event object to the event handler.
|
||||
|
||||
```html
|
||||
<button id="two">add_event_listener passes event</button>
|
||||
```
|
||||
|
||||
```python
|
||||
<py-script>
|
||||
from js import console, document
|
||||
from pyodide.ffi.wrappers import add_event_listener
|
||||
|
||||
def hello_args(*args):
|
||||
console.log(f"Hi! I got some args! {args}")
|
||||
|
||||
add_event_listener(document.getElementById("two"), "click", hello_args)
|
||||
</py-script>
|
||||
```
|
||||
|
||||
or using the `addEventListener` method of the DOM element:
|
||||
|
||||
```html
|
||||
<button id="three">add_event_listener passes event</button>
|
||||
```
|
||||
|
||||
```python
|
||||
<py-script>
|
||||
from js import console, document
|
||||
from pyodide.ffi import create_proxy
|
||||
|
||||
def hello_args(*args):
|
||||
console.log(f"Hi! I got some args! {args}")
|
||||
|
||||
document.getElementById("three").addEventListener("click", create_proxy(hello_args))
|
||||
</py-script>
|
||||
```
|
||||
|
||||
or using the PyScript Element class:
|
||||
|
||||
```html
|
||||
<button id="four">add_event_listener passes event</button>
|
||||
```
|
||||
|
||||
```python
|
||||
<py-script>
|
||||
from js import console
|
||||
from pyodide.ffi import create_proxy
|
||||
|
||||
def hello_args(*args):
|
||||
console.log(f"Hi! I got some args! {args}")
|
||||
|
||||
Element("four").element.addEventListener("click", create_proxy(hello_args))
|
||||
</py-script>
|
||||
```
|
||||
|
||||
## JavaScript to PyScript and From PyScript to JavaScript
|
||||
|
||||
If you're wondering about how to pass objects from JavaScript to PyScript and/or the other way around head over to the [Passing Objects](passing-objects.md) page.
|
||||
|
||||
|
||||
### Exporting all Global Python Objects
|
||||
|
||||
We can use our new `createObject` function to "export" the entire Python global object dictionary as a JavaScript object:
|
||||
|
||||
```python
|
||||
<py-script>
|
||||
from js import createObject
|
||||
from pyodide.ffi import create_proxy
|
||||
createObject(create_proxy(globals()), "pyodideGlobals")
|
||||
</py-script>
|
||||
```
|
||||
|
||||
This will make all Python global variables available in JavaScript with `pyodideGlobals.get('my_variable_name')`.
|
||||
|
||||
(Since PyScript tags evaluate _after_ all JavaScript on the page, we can't just dump a `console.log(...)` into a `<script>` tag, since that tag will evaluate before any PyScript has a chance to. We need to delay accessing the Python variable in JavaScript until after the Python code has a chance to run. The following example uses a button with `id="do-math"` to achieve this, but any method would be valid.)
|
||||
|
||||
```python
|
||||
<py-script>
|
||||
# create some Python objects:
|
||||
symbols = {'pi': 3.1415926, 'e': 2.7182818}
|
||||
|
||||
def rough_exponential(x):
|
||||
return symbols['e']**x
|
||||
|
||||
class Circle():
|
||||
def __init__(self, radius):
|
||||
self.radius = radius
|
||||
|
||||
@property
|
||||
def area:
|
||||
return symbols['pi'] * self.radius**2
|
||||
</py-script>
|
||||
```
|
||||
|
||||
```html
|
||||
<input type="button" value="Log Python Variables" id="do-mmath" />
|
||||
<script>
|
||||
document.getElementById("do-math").addEventListener("click", () => {
|
||||
const exp = pyodideGlobals.get("rough_exponential");
|
||||
console.log("e squared is about ${exp(2)}");
|
||||
const c = pyodideGlobals.get("Circle")(4);
|
||||
console.log("The area of c is ${c.area}");
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Exporting Individual Python Objects
|
||||
|
||||
We can also export individual Python objects to the JavaScript global scope if we wish.
|
||||
|
||||
(As above, the following example uses a button to delay the execution of the `<script>` until after the PyScript has run.)
|
||||
|
||||
```python
|
||||
<py-script>
|
||||
import js
|
||||
from pyodide.ffi import create_proxy
|
||||
|
||||
# Create 3 python objects
|
||||
language = "Python 3"
|
||||
animals = ['dog', 'cat', 'bird']
|
||||
multiply3 = lambda a, b, c: a * b * c
|
||||
|
||||
# js object can be named the same as Python objects...
|
||||
js.createObject(language, "language")
|
||||
|
||||
# ...but don't have to be
|
||||
js.createObject(create_proxy(animals), "animals_from_py")
|
||||
|
||||
# functions are objects too, in both Python and Javascript
|
||||
js.createObject(create_proxy(multiply3), "multiply")
|
||||
</py-script>
|
||||
```
|
||||
|
||||
```html
|
||||
<input type="button" value="Log Python Variables" id="log-python-variables" />
|
||||
<script>
|
||||
document
|
||||
.getElementById("log-python-variables")
|
||||
.addEventListener("click", () => {
|
||||
console.log(`Nice job using ${language}`);
|
||||
for (const animal of animals_from_py) {
|
||||
console.log(`Do you like ${animal}s? `);
|
||||
}
|
||||
console.log(`2 times 3 times 4 is ${multiply(2, 3, 4)}`);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
@@ -35,7 +35,8 @@ for dealing with the response, such as `json()` or `status`. See the
|
||||
[FetchResponse documentation](https://pyodide.org/en/stable/usage/api/python-api/http.html#pyodide.http.FetchResponse)
|
||||
for more information.
|
||||
|
||||
# Example
|
||||
## Example
|
||||
|
||||
We will make async HTTP requests to [JSONPlaceholder](https://jsonplaceholder.typicode.com/)'s fake API using `pyfetch`.
|
||||
First we write a helper function in pure Python that makes a request and returns the response. This function
|
||||
makes it easier to make specific types of requests with the most common parameters.
|
||||
@@ -70,6 +71,7 @@ async def request(url: str, method: str = "GET", body: Optional[str] = None,
|
||||
response = await pyfetch(url, **kwargs)
|
||||
return response
|
||||
```
|
||||
|
||||
This function is a wrapper for `pyfetch`, which is a wrapper for the `fetch` API. It is a coroutine function,
|
||||
so it must be awaited. It also has type hints, which are not required, but are useful for IDEs and other tools.
|
||||
The basic idea is that the `PyScript` will import and call this function, then await the response. Therefore,
|
||||
@@ -160,7 +162,8 @@ concluding html code.
|
||||
The very first thing to notice is the `py-config` tag. This tag is used to import Python files into the `PyScript`.
|
||||
In this case, we are importing the `request.py` file, which contains the `request` function we wrote above.
|
||||
|
||||
### `py-script` tag for making async HTTP requests.
|
||||
### `py-script` tag for making async HTTP requests
|
||||
|
||||
Next, the `py-script` tag contains the actual Python code where we import `asyncio` and `json`,
|
||||
which are required or helpful for the `request` function.
|
||||
The `# GET`, `# POST`, `# PUT`, `# DELETE` blocks show examples of how to use the `request` function to make basic
|
||||
@@ -169,6 +172,7 @@ HTTP requests. The `await` keyword is required not only for the `request` functi
|
||||
faster ones.
|
||||
|
||||
### HTTP Requests
|
||||
|
||||
HTTP requests are a very common way to communicate with a server. They are used for everything from getting data from
|
||||
a database, to sending emails, to authorization, and more. Due to safety concerns, files loaded from the
|
||||
local file system are not accessible by `PyScript`. Therefore, the proper way to load data into `PyScript` is also
|
||||
@@ -182,31 +186,38 @@ function or to `pyfetch`. See the
|
||||
HTTP requests are defined by standards-setting bodies in [RFC 1945](https://www.rfc-editor.org/info/rfc1945) and
|
||||
[RFC 9110](https://www.rfc-editor.org/info/rfc9110).
|
||||
|
||||
# Conclusion
|
||||
## Conclusion
|
||||
|
||||
This tutorial demonstrates how to make HTTP requests using `pyfetch` and the `FetchResponse` objects. Importing Python
|
||||
code/files into the `PyScript` using the `py-config` tag is also covered.
|
||||
|
||||
Although a simple example, the principals here can be used to create complex web applications inside of `PyScript`,
|
||||
or load data into `PyScript` for use by an application, all served as a static HTML page, which is pretty amazing!
|
||||
|
||||
## API Quick Reference
|
||||
|
||||
# API Quick Reference
|
||||
## pyodide.http.pyfetch
|
||||
### Usage
|
||||
|
||||
### pyfetch Usage
|
||||
|
||||
```python
|
||||
await pyodide.http.pyfetch(url: str, **kwargs: Any) -> FetchResponse
|
||||
```
|
||||
|
||||
Use `pyfetch` to make HTTP requests in `PyScript`. This is a wrapper around the `fetch` API. Returns a `FetchResponse`.
|
||||
|
||||
- [`pyfetch` Docs.](https://pyodide.org/en/stable/usage/api/python-api/http.html#pyodide.http.pyfetch)
|
||||
|
||||
## pyodide.http.FetchResponse
|
||||
### Usage
|
||||
|
||||
### FetchResponse Usage
|
||||
|
||||
```python
|
||||
response: pyodide.http.FetchResponse = await <pyfetch call>
|
||||
status = response.status
|
||||
json = await response.json()
|
||||
```
|
||||
|
||||
Class for handling HTTP responses. This is a wrapper around the `JavaScript` fetch `Response`. Contains common (async)
|
||||
methods and properties for handling HTTP responses, such as `json()`, `url`, `status`, `headers`, etc.
|
||||
|
||||
|
||||
@@ -18,4 +18,5 @@ passing-objects
|
||||
http-requests
|
||||
asyncio
|
||||
custom-plugins
|
||||
event-handlers
|
||||
```
|
||||
|
||||
@@ -5,15 +5,13 @@ PyScript provides a convenient syntax for mapping JavaScript events to PyScript
|
||||
For example, you can use the following code to connect the click event to a button:
|
||||
|
||||
```
|
||||
<button id="py-click" py-onClick="foo()">Click me</button>
|
||||
<button id="py-click" py-click="foo()">Click me</button>
|
||||
```
|
||||
|
||||
Here is a list of all the available event mappings:
|
||||
|
||||
| PyScript Event Name | DOM Event Name |
|
||||
|-------------------|----------------|
|
||||
| py-onClick | click |
|
||||
| py-onKeyDown | keydown |
|
||||
| py-afterprint | afterprint |
|
||||
| py-beforeprint | beforeprint |
|
||||
| py-beforeunload | beforeunload |
|
||||
|
||||
@@ -35,7 +35,7 @@ Display will throw an exception if the target is not clear. E.g. the following c
|
||||
# from event handlers
|
||||
display('hello')
|
||||
</py-script>
|
||||
<button id="my-button" py-onClick="display_hello()">Click me</button>
|
||||
<button id="my-button" py-click="display_hello()">Click me</button>
|
||||
```
|
||||
|
||||
Because it's considered unclear if the `hello` string should be displayed underneath the `<py-script>` tag or the `<button>` tag.
|
||||
@@ -45,12 +45,11 @@ To write compliant code, make sure to specify the target using the `target` para
|
||||
```html
|
||||
<py-script>
|
||||
def display_hello():
|
||||
# this fails because we don't have any implicit target
|
||||
# from event handlers
|
||||
# this works because we give an explicit target
|
||||
display('hello', target="helloDiv")
|
||||
</py-script>
|
||||
<div id="helloDiv"></div>
|
||||
<button id="my-button" py-onClick="display_hello()">Click me</button>
|
||||
<button id="my-button" py-click="display_hello()">Click me</button>
|
||||
```
|
||||
|
||||
#### Using matplotlib with display
|
||||
|
||||
51
docs/reference/API/when.md
Normal file
51
docs/reference/API/when.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# `@when`
|
||||
|
||||
`@when(event_type:str = None, selector:str = None)`
|
||||
|
||||
The `@when` decorator attaches the decorated function or Callable as an event handler for selected objects on the page. That is, when the named event is emitted by the selected DOM elements, the decorated Python function will be called.
|
||||
|
||||
If the decorated function takes a single (non-self) argument, it will be passed the [Event object](https://developer.mozilla.org/en-US/docs/Web/API/Event) corresponding to the triggered event. If the function takes no (non-self) argument, it will be called with no arguments.
|
||||
|
||||
## Parameters
|
||||
|
||||
`event_type` - A string representing the event type to match against. This can be any of the [https://developer.mozilla.org/en-US/docs/Web/Events#event_listing](https://developer.mozilla.org/en-US/docs/Web/Events) that HTML elements may emit, as appropriate to their element type.
|
||||
|
||||
`selector` = A string containing one or more [CSS selectors](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors). The selected DOM elements will have the decorated function attacehed as an event handler.
|
||||
|
||||
## Examples:
|
||||
|
||||
The following example prints "Hello, world!" whenever the button is clicked. It demonstrates using the `@when` decorator on a Callable which takes no arguments:
|
||||
|
||||
```html
|
||||
<button id="my_btn">Click Me to Say Hi</button>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
@when("click", selector="#my_btn")
|
||||
def say_hello():
|
||||
print(f"Hello, world!")
|
||||
</py-script>
|
||||
```
|
||||
|
||||
The following example includes three buttons - when any of the buttons is clicked, that button turns green, and the remaining two buttons turn red. This demonstrates using the `@when` decorator on a Callable which takes one argument, which is then passed the Event object from the associated event. When combined with the ability to look at other elements in on the page, this is quite a powerful feature.
|
||||
|
||||
```html
|
||||
<div id="container">
|
||||
<button>First</button>
|
||||
<button>Second</button>
|
||||
<button>Third</button>
|
||||
</div>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
import js
|
||||
|
||||
@when("click", selector="#container button")
|
||||
def highlight(evt):
|
||||
#Set the clicked button's background to green
|
||||
evt.target.style.backgroundColor = 'green'
|
||||
|
||||
#Set the background of all buttons to red
|
||||
other_buttons = (button for button in js.document.querySelectorAll('button') if button != evt.target)
|
||||
for button in other_buttons:
|
||||
button.style.backgroundColor = 'red'
|
||||
</py-script>
|
||||
```
|
||||
@@ -9,8 +9,8 @@ The `<py-config>` element should be placed within the `<body>` element.
|
||||
## Attributes
|
||||
|
||||
| attribute | type | default | description |
|
||||
|-----------|--------|---------|---------------------------------------------------------------------------------------------------------|
|
||||
| **type** | string | "toml" | Syntax type of the `<py-config>`. Value can be `json` or `toml`. Default: "toml" if type is unspecifed. |
|
||||
|-----------|--------|---------|----------------------------------------------------------------------------------------------------------|
|
||||
| **type** | string | "toml" | Syntax type of the `<py-config>`. Value can be `json` or `toml`. Default: "toml" if type is unspecified. |
|
||||
| **src** | url | | Source url to an external configuration file. |
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -5,26 +5,66 @@ The `<py-repl>` element provides a REPL(Read Eval Print Loop) to evaluate multi-
|
||||
## Attributes
|
||||
|
||||
| attribute | type | default | description |
|
||||
|-------------------|---------|---------|---------------------------------------|
|
||||
|-------------------|---------|---------|--------------------------------------|
|
||||
| **auto-generate** | boolean | | Auto-generates REPL after evaluation |
|
||||
| **output** | string | | The element to write output into |
|
||||
| **output-mode** | string | "" | Determines whether the output element is cleared prior to writing output |
|
||||
| **output** | string | | The id of the element to write `stdout` and `stderr` to |
|
||||
| **stderr** | string | | The id of the element to write `stderr` to |
|
||||
| **src** | string | | Resource to be preloaded into the REPL |
|
||||
|
||||
### Examples
|
||||
|
||||
#### `<py-repl>` element set to auto-generate
|
||||
### `auto-generate`
|
||||
If a \<py-repl\> tag has the `auto-generate` attribute, upon execution, another \<pr-repl\> tag will be created and added to the DOM as a sibling of the current tag.
|
||||
|
||||
### `output-mode`
|
||||
By default, the element which displays the output from a REPL is cleared (`innerHTML` set to "") prior to each new execution of the REPL. If `output-mode` == "append", that element is not cleared, and the output is appended instead.
|
||||
|
||||
### `output`
|
||||
The ID of an element in the DOM that `stdout` (e.g. `print()`), `stderr`, and the results of executing the repl are written to. Defaults to an automatically-generated \<div\> as the next sibling of the REPL itself.
|
||||
|
||||
### `stderr`
|
||||
The ID of an element in the DOM that `stderr` will be written to. Defaults to None, though writes to `stderr` will still appear in the location specified by `output`.
|
||||
|
||||
### `src`
|
||||
If a \<py-repl\> tag has the `src` attribute, during page initialization, resource in the `src` will be preloaded into the REPL. Please note that this will not run in advance. If there is content in the \<py-repl\> tag, it will be cleared and replaced with preloaded resource.
|
||||
|
||||
## Methods
|
||||
|
||||
The following are methods that can be called on the \<py-repl\> element, from within Python or JavaScript
|
||||
|
||||
### `getPySrc()`
|
||||
|
||||
Returns the current code contents of the REPL as a string.
|
||||
|
||||
## Examples
|
||||
|
||||
### `<py-repl>` element set to auto-generate
|
||||
|
||||
```html
|
||||
<py-repl auto-generate="true"> </py-repl>
|
||||
```
|
||||
|
||||
#### `<py-repl>` element with output
|
||||
### `<py-repl>` element with output
|
||||
|
||||
The following will write "Hello! World!" to the div with id `replOutput`.
|
||||
|
||||
```html
|
||||
<div id="replOutput"></div>
|
||||
<py-repl output="replOutput">
|
||||
hello = "Hello world!"
|
||||
print("Hello!")
|
||||
hello = "World!"
|
||||
hello
|
||||
</py-repl>
|
||||
```
|
||||
|
||||
Note that if we `print` any element in the repl, the output will be printed in the [`py-terminal`](../plugins/py-terminal.md) if is enabled.
|
||||
Note that if we `print` from the REPL (or otherwise write to `sys.stdout`), the output will be printed in the [`py-terminal`](../plugins/py-terminal.md) if is enabled.
|
||||
|
||||
### `<py-repl>` element with src
|
||||
Preload resource from src into the REPL
|
||||
```html
|
||||
<py-repl id="py-repl" output="replOutput" src="./src/py/py_code.py">
|
||||
If a py-repl tag has the src attribute,
|
||||
the content here will be cleared and replaced.
|
||||
</py-repl>
|
||||
<div id="replOutput"></div>
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# <py-script>
|
||||
|
||||
The `<py-script>` element lets you execute multi-line Python scripts both inline and via a src attribute.
|
||||
The `<py-script>` element, also available as `<script type="py-script">`, lets you execute multi-line Python scripts both inline and via a src attribute.
|
||||
|
||||
## Attributes
|
||||
|
||||
@@ -12,13 +12,13 @@ The `<py-script>` element lets you execute multi-line Python scripts both inline
|
||||
|
||||
### output
|
||||
|
||||
If the `output` attribute is provided, any output to [sys.stdout](https://docs.python.org/3/library/sys.html#sys.stdout) or [sys.stderr](https://docs.python.org/3/library/sys.html#sys.stderr) is written to the DOM element with the ID matching the attribute. If no DOM element is found with a matching ID, a warning is shown. The msg is output to the `innerHTML` of the HTML Element, with newlines (`\n'`) converted to breaks (`<br\>`).
|
||||
If the `output` attribute is provided, any output to [sys.stdout](https://docs.python.org/3/library/sys.html#sys.stdout) or [sys.stderr](https://docs.python.org/3/library/sys.html#sys.stderr) is written to the DOM element with the ID matching the attribute. If no DOM element is found with a matching ID, a warning is shown. The msg is output to the `innerHTML` of the HTML Element, with newlines (`\n'`) converted to breaks (`<br/>`).
|
||||
|
||||
This output is in addition to the output being written to the developer console and the `<py-terminal>` if it is being used.
|
||||
|
||||
### stderr
|
||||
|
||||
If the `stderr` attribute is provided, any output to [sys.stderr](https://docs.python.org/3/library/sys.html#sys.stderr) is written to the DOM element with the ID matching the attribute. If no DOM element is found with a matching ID, a warning is shown. The msg is output to the `innerHTML` of the HTML Element, with newlines (`\n'`) converted to breaks (`<br\>`).
|
||||
If the `stderr` attribute is provided, any output to [sys.stderr](https://docs.python.org/3/library/sys.html#sys.stderr) is written to the DOM element with the ID matching the attribute. If no DOM element is found with a matching ID, a warning is shown. The msg is output to the `innerHTML` of the HTML Element, with newlines (`\n'`) converted to breaks (`<br/>`).
|
||||
|
||||
This output is in addition to the output being written to the developer console and the `<py-terminal>` if it is being used.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# pyscript
|
||||
|
||||
The code underlying PyScript is a TypeScript/JavaScript module, which is loaded and executed by the browser. This is what loads when you include, for example, `<script defer src="https://pyscript.net/latest/pyscript.js">` in your HTML.
|
||||
The code underlying PyScript is a JavaScript module, which is loaded and executed by the browser. This is what loads when you include, for example, `<script defer src="https://pyscript.net/latest/pyscript.js">` in your HTML.
|
||||
|
||||
The module is exported to the browser as `pyscript`. The exports from this module are:
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ This is one of the core plugins in PyScript, which is active by default. With it
|
||||
|
||||
## Configuration
|
||||
|
||||
You can control how `<py-terminal>` behaves by setting the value of the `terminal` configuration in your `<py-config>`.
|
||||
You can control how `<py-terminal>` behaves by setting the values of the `terminal`, `docked`, and `xterm` fields in your configuration in your `<py-config>`.
|
||||
|
||||
For the **terminal** field, these are the values:
|
||||
|
||||
| value | description |
|
||||
|-------|-------------|
|
||||
@@ -12,11 +14,50 @@ You can control how `<py-terminal>` behaves by setting the value of the `termin
|
||||
| `true` | Automatically add a `<py-terminal>` to the page |
|
||||
| `"auto"` | This is the default. Automatically add a `<py-terminal auto>`, to the page. The terminal is initially hidden and automatically shown as soon as something writes to `stdout` and/or `stderr` |
|
||||
|
||||
For the **docked** field, these are the values:
|
||||
|
||||
| value | description |
|
||||
|-------|-------------|
|
||||
| `false` | Don't dock `<py-terminal>` to the page |
|
||||
| `true` | Automatically dock a `<py-terminal>` to the page |
|
||||
| `"docked"` | This is the default. Automatically add a `<py-terminal docked>`, to the page. The terminal, once visible, is automatically shown at the bottom of the page, covering the width of such page |
|
||||
|
||||
Please note that **docked** mode is currently used as default only when `terminal="auto"`, or *terminal* default, is used.
|
||||
|
||||
In all other cases it's up to the user decide if a terminal should be docked or not.
|
||||
|
||||
For the **xterm** field, these are the values:
|
||||
|
||||
| value | description |
|
||||
|-------|-------------|
|
||||
| `false` | This is the default. The `<py-terminal>` is a simple `<pre>` tag with some CSS styling. |
|
||||
| `true` or `xterm` | The [xtermjs](http://xtermjs.org/) library is loaded and its Terminal object is used as the `<py-terminal>`. It's visibility and position are determined by the `docked` and `auto` keys in the same way as the default `<py-terminal>` |
|
||||
|
||||
The xterm.js [Terminal object](http://xtermjs.org/docs/api/terminal/classes/terminal/) can be accessed directly if you want to adjust its properties, add [custom parser hooks](http://xtermjs.org/docs/guides/hooks/), introduce [xterm.js addons](http://xtermjs.org/docs/guides/using-addons/), etc. Access is best achieved by awaiting the `xtermReady` attribute of the `<py-terminal>` HTML element itself:
|
||||
|
||||
```python
|
||||
import js
|
||||
import asyncio
|
||||
|
||||
async def adjust_term_size(columns, rows):
|
||||
xterm = await js.document.querySelector('py-terminal').xtermReady
|
||||
xterm.resize(columns, rows)
|
||||
|
||||
asyncio.ensure_future(adjust_term_size(40,10))
|
||||
```
|
||||
|
||||
Some terminal-formatting packages read from specific environment variables to determine whether they should emit formatted output; PyScript does not set these variables explicitly - you may need to set them yourself, or force your terminal-formatting package into a state where it outputs correctly formatted output.
|
||||
|
||||
A couple of specific examples:
|
||||
- the [rich](https://github.com/Textualize/rich) will not, by default, output colorful text, but passing `256` or `truecolor` as an argument as the `color_system` parameter to the [Console constructor](https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console) will force it to do so. (As of rich v13)
|
||||
- [termcolor](https://github.com/termcolor/termcolor) will not, by default, output colorful text, but setting `os.environ["FORCE_COLOR"] = "True"` or by passing `force_color=True` to the `colored()` function will force it to do so. (As of termcolor v2.3)
|
||||
|
||||
### Examples
|
||||
|
||||
```html
|
||||
<py-config>
|
||||
terminal = true
|
||||
docked = false
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
|
||||
@@ -5,7 +5,7 @@ This page will guide you through getting started with PyScript.
|
||||
## Development setup
|
||||
|
||||
PyScript does not require any development environment other
|
||||
then a web browser (we recommend using [Chrome](https://www.google.com/chrome/)) and a text editor, even though using your [IDE](https://en.wikipedia.org/wiki/Integrated_development_environment) of choice might be convenient.
|
||||
than a web browser (we recommend using [Chrome](https://www.google.com/chrome/)) and a text editor, even though using your [IDE](https://en.wikipedia.org/wiki/Integrated_development_environment) of choice might be convenient.
|
||||
|
||||
If you're using [VSCode](https://code.visualstudio.com/), the
|
||||
[Live Server extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)
|
||||
@@ -42,6 +42,12 @@ open an HTML by double-clicking it in your file explorer.
|
||||
</html>
|
||||
```
|
||||
|
||||
### Using a Local Server
|
||||
|
||||
In some situations, your browser may forbid loading remote resources like `pyscript.js` and `pyscript.css` when you open an HTML file directly. When this is the case, you may see your Python code in the text of the webpage, and the [browser developer console](https://balsamiq.com/support/faqs/browserconsole/) may show an error like *"Cross origin requests are only supported for HTTP."* The fix for this is to use a [simple local server](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/set_up_a_local_testing_server) to make your html file available to the browser.
|
||||
|
||||
If you have python installed on your system, you can use it's basic built-in server for this purpose via the command line. Change the current working directory of your terminal or command line to the folder where your HTML file is stored. From this folder, run `python -m http.server 8080 --bind 127.0.0.1` in your terminal or command line. With the server program running, point your browser to `http://localhost:8080` to view the contents of that folder. (If a file in that folder is called `index.html`, it will be displayed by default.)
|
||||
|
||||
## A more complex example
|
||||
|
||||
Now that we know how you can create a simple 'Hello, World!' example, let's see a more complex example. This example will use the Demo created by [Cheuk Ting Ho](https://github.com/Cheukting). In this example, we will use more features from PyScript.
|
||||
@@ -249,8 +255,8 @@ Now that we have a way to explore the data using `py-repl` and a way to create t
|
||||
<label for="all"> All 🍧</label>
|
||||
<input type="radio" id="chocolate" name="flavour" value="COCOA">
|
||||
<label for="chocolate"> Chocolate 🍫</label>
|
||||
<input type="radio" id="cherrie" name="flavour" value="CHERRIES">
|
||||
<label for="cherrie"> Cherries 🍒</label>
|
||||
<input type="radio" id="cherry" name="flavour" value="CHERRIES">
|
||||
<label for="cherry"> Cherries 🍒</label>
|
||||
<input type="radio" id="berries" name="flavour" value="BERRY">
|
||||
<label for="berries"> Berries 🍓</label>
|
||||
<input type="radio" id="cheese" name="flavour" value="CHEESE">
|
||||
|
||||
@@ -35,5 +35,4 @@ glob:
|
||||
---
|
||||
py-config-fetch
|
||||
py-config-interpreter
|
||||
writing-to-page
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Handling click events
|
||||
|
||||
This tutorial will show you how to use the `py-click` attribute to handle mouse clicks on elements on your page. The `py-click` attribute is a special attribute that allows you to specify a Python function that will be called when the element is clicked.
|
||||
This tutorial will show you how to use the `py-click` attribute to handle mouse clicks on elements on your page. The `py-click` attribute is a special attribute that allows you to specify a Python function that will be called when the element is clicked. There are many other events such as py-mouseover, py-focus, py-input, py-keyress etc, which can be used as well. They are listed here [Attr-to-Event](../reference/API/attr_to_event.html)
|
||||
|
||||
## Development setup
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ To get started, let's create a new `index.html` file and import `pyscript.js`.
|
||||
</html>
|
||||
```
|
||||
|
||||
We are using the pyodide CDN to setup our interpreter, but you can also download the files from [the pyodide GitHub release](https://github.com/pyodide/pyodide/releases/tag/0.22.0a3), unzip them and use the `pyodide.js` file as your interpreter.
|
||||
We are using the pyodide CDN to setup our interpreter, but you can also download the files from [the pyodide GitHub releases](https://github.com/pyodide/pyodide/releases/), unzip them and use the `pyodide.js` file as your interpreter.
|
||||
|
||||
## Setting the interpreter
|
||||
|
||||
@@ -47,8 +47,8 @@ To set the interpreter, you can use the `interpreter` configuration in the `py-c
|
||||
<body>
|
||||
<py-config>
|
||||
[[interpreters]]
|
||||
src = "https://cdn.jsdelivr.net/pyodide/v0.22.0a3/full/pyodide.js"
|
||||
name = "pyodide-0.22.0a3"
|
||||
src = "https://cdn.jsdelivr.net/pyodide/v0.23.0/full/pyodide.js"
|
||||
name = "pyodide-0.23.0"
|
||||
lang = "python"
|
||||
</py-config>
|
||||
</body>
|
||||
@@ -75,8 +75,8 @@ To confirm that the interpreter is set correctly, you can open the DevTools and
|
||||
<body>
|
||||
<py-config>
|
||||
[[interpreters]]
|
||||
src = "https://cdn.jsdelivr.net/pyodide/v0.22.0a3/full/pyodide.js"
|
||||
name = "pyodide-0.22.0a3"
|
||||
src = "https://cdn.jsdelivr.net/pyodide/v0.23.0/full/pyodide.js"
|
||||
name = "pyodide-0.23.0"
|
||||
lang = "python"
|
||||
</py-config>
|
||||
<py-script>
|
||||
|
||||
@@ -72,10 +72,10 @@ Now that we have installed the dependencies, we need to patch the Requests libra
|
||||
|
||||
<py-script>
|
||||
import pyodide_http
|
||||
pyodide_http.patch()
|
||||
pyodide_http.patch_all()
|
||||
</py-script>
|
||||
</body>
|
||||
</html
|
||||
</html>
|
||||
```
|
||||
|
||||
## Making a request
|
||||
@@ -104,7 +104,7 @@ Finally, let's make a request to the JSON Placeholder API to confirm that everyt
|
||||
import pyodide_http
|
||||
|
||||
# Patch the Requests library so it works with Pyscript
|
||||
pyodide_http.patch()
|
||||
pyodide_http.patch_all()
|
||||
|
||||
# Make a request to the JSON Placeholder API
|
||||
response = requests.get("https://jsonplaceholder.typicode.com/todos")
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"vega_datasets"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
<py-script>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<section class="pyscript">
|
||||
<py-config>
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
[[fetch]]
|
||||
files = ["./antigravity.py"]
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"xyzservices"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"numpy",
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<py-tutor modules="d3.py">
|
||||
<py-config>
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
[[fetch]]
|
||||
files = ["./d3.py"]
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"pandas"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
|
||||
@@ -11,16 +11,19 @@
|
||||
rel="stylesheet"
|
||||
href="https://pyscript.net/latest/pyscript.css"
|
||||
/>
|
||||
|
||||
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
|
||||
<script defer src="../../pyscriptjs/build/pyscript.js"></script>
|
||||
<!-- <script defer src="https://pyscript.net/latest/pyscript.js"></script> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<py-script>
|
||||
from js import handTrack, requestAnimationFrame
|
||||
from js import handTrack, requestAnimationFrame, console
|
||||
from pyodide import create_once_callable
|
||||
import asyncio
|
||||
|
||||
update_note = Element("update-note")
|
||||
canvas = Element("canvas")
|
||||
video = Element("myvideo")
|
||||
context = canvas.element.getContext("2d")
|
||||
|
||||
isVideo = False
|
||||
@@ -33,7 +36,7 @@
|
||||
"scoreThreshold": 0.6, # confidence threshold for predictions.
|
||||
}
|
||||
|
||||
def toggle_video(evt):
|
||||
def toggle_video():
|
||||
global isVideo
|
||||
if (not isVideo):
|
||||
update_note.write("Starting video")
|
||||
@@ -112,7 +115,7 @@
|
||||
id="trackbutton"
|
||||
class="bx--btn bx--btn--secondary"
|
||||
type="button"
|
||||
py-onClick="toggle_video()"
|
||||
py-click="toggle_video()"
|
||||
>
|
||||
Toggle Video
|
||||
</button>
|
||||
@@ -124,13 +127,11 @@
|
||||
>
|
||||
Next Image
|
||||
</button>
|
||||
<div id="update-note" py-mount class="updatenote mt10">
|
||||
loading model ..
|
||||
</div>
|
||||
<div id="update-note" class="updatenote mt10">loading model ..</div>
|
||||
</div>
|
||||
<div>
|
||||
<video autoplay="autoplay" id="myvideo" py-mount="video"></video>
|
||||
<canvas id="canvas" py-mount class="border canvasbox"></canvas>
|
||||
<canvas id="canvas" class="border canvasbox"></canvas>
|
||||
</div>
|
||||
<script src="lib/handtrack.min.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<py-tutor>
|
||||
<py-config>
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
from pyodide import create_once_callable
|
||||
import asyncio
|
||||
|
||||
update_note = Element("update-note")
|
||||
canvas = Element("canvas")
|
||||
video = Element("myvideo")
|
||||
context = canvas.element.getContext("2d")
|
||||
|
||||
isVideo = False
|
||||
@@ -60,7 +63,7 @@
|
||||
"scoreThreshold": 0.6, # confidence threshold for predictions.
|
||||
}
|
||||
|
||||
def toggle_video(evt):
|
||||
def toggle_video():
|
||||
global isVideo
|
||||
player.jump()
|
||||
|
||||
@@ -140,7 +143,7 @@
|
||||
id="trackbutton"
|
||||
class="bx--btn bx--btn--secondary"
|
||||
type="button"
|
||||
py-onClick="toggle_video()"
|
||||
py-click="toggle_video()"
|
||||
>
|
||||
Start Video
|
||||
</button>
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
<title>PyMarkdown</title>
|
||||
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link rel="stylesheet" href="../build/pyscript.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://pyscript.net/latest/pyscript.css"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="./assets/css/examples.css" />
|
||||
<script defer src="../build/pyscript.js"></script>
|
||||
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -19,8 +22,8 @@
|
||||
"markdown"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_markdown.py",
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_markdown.py",
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
"matplotlib"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
<script type="py">
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.tri as tri
|
||||
import numpy as np
|
||||
@@ -66,7 +66,7 @@
|
||||
ax1.set_title('tripcolor of Delaunay triangulation, flat shading')
|
||||
|
||||
display(fig1, target="mpl")
|
||||
</py-script>
|
||||
</script>
|
||||
</py-tutor>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"matplotlib"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
id="run-all-button"
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
py-onClick="run_all_micrograd_demo()"
|
||||
py-click="run_all_micrograd_demo()"
|
||||
>
|
||||
Run All</button
|
||||
><br />
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
}
|
||||
</py-config>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<py-tutor>
|
||||
<py-config>
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
packages = ["pandas"]
|
||||
</py-config>
|
||||
@@ -101,7 +101,7 @@
|
||||
df = pd.DataFrame()
|
||||
|
||||
|
||||
def loadFromURL(*ags, **kws):
|
||||
def loadFromURL(*args, **kws):
|
||||
global df
|
||||
|
||||
# clear dataframe & output
|
||||
|
||||
@@ -3,27 +3,10 @@
|
||||
<title>Panel Example</title>
|
||||
<meta charset="iso-8859-1" />
|
||||
<link rel="icon" type="image/x-icon" href="./favicon.png" />
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.bokeh.org/bokeh/release/bokeh-2.4.3.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.3.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.3.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.jsdelivr.net/npm/@holoviz/panel@0.14.1/dist/panel.min.js"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://pyscript.net/latest/pyscript.css"
|
||||
/>
|
||||
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
|
||||
<link rel="stylesheet" href="./assets/css/examples.css" />
|
||||
</head>
|
||||
<body>
|
||||
@@ -41,6 +24,11 @@
|
||||
<div id="simple_app"></div>
|
||||
|
||||
<py-tutor>
|
||||
<script defer src="https://cdn.bokeh.org/bokeh/release/bokeh-2.4.3.js"></script>
|
||||
<script defer src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.3.min.js"></script>
|
||||
<script defer src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.3.min.js"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@holoviz/panel@0.14.1/dist/panel.min.js"></script>
|
||||
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
|
||||
<py-config>
|
||||
packages = [
|
||||
"https://cdn.holoviz.org/panel/0.14.3/dist/wheels/bokeh-2.4.3-py3-none-any.whl",
|
||||
@@ -48,7 +36,7 @@
|
||||
"panel==0.14.1"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
"panel==0.13.1"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
"panel==0.13.1"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
import altair as alt
|
||||
import panel as pn
|
||||
import pandas as pd
|
||||
import param
|
||||
|
||||
from sklearn.cluster import KMeans
|
||||
from pyodide.http import open_url
|
||||
@@ -170,7 +171,7 @@
|
||||
y=alt.Y(y, scale=alt.Scale(zero=False)),
|
||||
shape='labels',
|
||||
color='species'
|
||||
).add_selection(brush).properties(width=800) +
|
||||
).add_params(brush).properties(width=800) +
|
||||
alt.Chart(centers)
|
||||
.mark_point(size=250, shape='cross', color='black')
|
||||
.encode(x=x+':Q', y=y+':Q')
|
||||
@@ -197,8 +198,8 @@
|
||||
@pn.depends(x, y, n_clusters, watch=True)
|
||||
def update_chart(*events):
|
||||
chart.object = get_chart(x.value, y.value, table.value)
|
||||
chart.selection.param.watch(update_filters, 'brush')
|
||||
|
||||
@param.depends('brush', watch=True)
|
||||
def update_filters(event=None):
|
||||
filters = []
|
||||
for k, v in (getattr(event, 'new') or {}).items():
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"panel==0.13.1"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
|
||||
170
examples/py_list.py
Normal file
170
examples/py_list.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import time
|
||||
from datetime import datetime as dt
|
||||
from textwrap import dedent
|
||||
|
||||
import js
|
||||
from pyscript import Element, Plugin, create
|
||||
|
||||
plugin = Plugin("PyList")
|
||||
|
||||
|
||||
class PyItemTemplate(Element):
|
||||
label_fields = None
|
||||
|
||||
def __init__(self, data, labels=None, state_key=None, parent=None):
|
||||
self.data = data
|
||||
|
||||
self.register_parent(parent)
|
||||
|
||||
if not labels:
|
||||
labels = list(self.data.keys())
|
||||
self.labels = labels
|
||||
|
||||
self.state_key = state_key
|
||||
|
||||
super().__init__(self._id)
|
||||
|
||||
def register_parent(self, parent):
|
||||
self._parent = parent
|
||||
if parent:
|
||||
self._id = f"{self._parent._id}-c-{len(self._parent._children)}"
|
||||
self.data["id"] = self._id
|
||||
else:
|
||||
self._id = None
|
||||
|
||||
def create(self):
|
||||
new_child = create("div", self._id, "py-li-element")
|
||||
new_child._element.innerHTML = dedent(
|
||||
f"""
|
||||
<label id="{self._id}" for="flex items-center p-2 ">
|
||||
<input class="mr-2" type="checkbox" class="task-check">
|
||||
<p>{self.render_content()}</p>
|
||||
</label>
|
||||
"""
|
||||
)
|
||||
return new_child
|
||||
|
||||
def on_click(self, evt):
|
||||
pass
|
||||
|
||||
def pre_append(self):
|
||||
pass
|
||||
|
||||
def post_append(self):
|
||||
self.element.click = self.on_click
|
||||
self.element.onclick = self.on_click
|
||||
self._post_append()
|
||||
|
||||
def _post_append(self):
|
||||
pass
|
||||
|
||||
def strike(self, value, extra=None):
|
||||
if value:
|
||||
self.add_class("line-through")
|
||||
else:
|
||||
self.remove_class("line-through")
|
||||
|
||||
def render_content(self):
|
||||
return " - ".join([self.data[f] for f in self.labels])
|
||||
|
||||
|
||||
class PyListTemplate:
|
||||
item_class = PyItemTemplate
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self._children = []
|
||||
self._id = self.parent.id
|
||||
self.main_style_classes = "py-li-element"
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return [c.data for c in self._children]
|
||||
|
||||
def render_children(self):
|
||||
binds = {}
|
||||
for i, c in enumerate(self._children):
|
||||
txt = c.element.innerHTML
|
||||
rnd = str(time.time()).replace(".", "")[-5:]
|
||||
new_id = f"{c.element.id}-{i}-{rnd}"
|
||||
binds[new_id] = c.element.id
|
||||
txt = txt.replace(">", f" id='{new_id}'>")
|
||||
print(txt)
|
||||
|
||||
def foo(evt):
|
||||
evtEl = evt.srcElement
|
||||
srcEl = Element(binds[evtEl.id])
|
||||
srcEl.element.onclick()
|
||||
evtEl.classList = srcEl.element.classList
|
||||
|
||||
for new_id in binds:
|
||||
Element(new_id).element.onclick = foo
|
||||
|
||||
def connect(self):
|
||||
self.md = main_div = js.document.createElement("div")
|
||||
main_div.id = self._id + "-list-tasks-container"
|
||||
|
||||
if self.main_style_classes:
|
||||
for klass in self.main_style_classes.split(" "):
|
||||
main_div.classList.add(klass)
|
||||
|
||||
self.parent.appendChild(main_div)
|
||||
|
||||
def add(self, *args, **kws):
|
||||
if not isinstance(args[0], self.item_class):
|
||||
child = self.item_class(*args, **kws)
|
||||
else:
|
||||
child = args[0]
|
||||
child.register_parent(self)
|
||||
return self._add(child)
|
||||
|
||||
def _add(self, child_elem):
|
||||
self.pre_child_append(child_elem)
|
||||
child_elem.pre_append()
|
||||
self._children.append(child_elem)
|
||||
self.md.appendChild(child_elem.create().element)
|
||||
child_elem.post_append()
|
||||
self.child_appended(child_elem)
|
||||
return child_elem
|
||||
|
||||
def pre_child_append(self, child):
|
||||
pass
|
||||
|
||||
def child_appended(self, child):
|
||||
"""Overwrite me to define logic"""
|
||||
pass
|
||||
|
||||
|
||||
class PyItem(PyItemTemplate):
|
||||
def on_click(self, evt=None):
|
||||
self.data["done"] = not self.data["done"]
|
||||
self.strike(self.data["done"])
|
||||
|
||||
self.select("input").element.checked = self.data["done"]
|
||||
|
||||
|
||||
class PyList(PyListTemplate):
|
||||
item_class = PyItem
|
||||
|
||||
def add(self, item):
|
||||
if isinstance(item, str):
|
||||
item = {"content": item, "done": False, "created_at": dt.now()}
|
||||
|
||||
super().add(item, labels=["content"], state_key="done")
|
||||
|
||||
|
||||
@plugin.register_custom_element("py-list")
|
||||
class PyListPlugin:
|
||||
def __init__(self, element):
|
||||
self.element = element
|
||||
self.py_list = PyList(self.element)
|
||||
|
||||
def add(self, item):
|
||||
self.py_list.add(item)
|
||||
|
||||
def connect(self):
|
||||
self.py_list.connect()
|
||||
@@ -1,21 +0,0 @@
|
||||
from datetime import datetime as dt
|
||||
|
||||
import pyscript
|
||||
|
||||
|
||||
class PyItem(pyscript.PyItemTemplate):
|
||||
def on_click(self, evt=None):
|
||||
self.data["done"] = not self.data["done"]
|
||||
self.strike(self.data["done"])
|
||||
|
||||
self.select("input").element.checked = self.data["done"]
|
||||
|
||||
|
||||
class PyList(pyscript.PyListTemplate):
|
||||
item_class = PyItem
|
||||
|
||||
def add(self, item):
|
||||
if isinstance(item, str):
|
||||
item = {"content": item, "done": False, "created_at": dt.now()}
|
||||
|
||||
super().add(item, labels=["content"], state_key="done")
|
||||
@@ -33,7 +33,7 @@
|
||||
<py-tutor modules="antigravity.py">
|
||||
<py-config>
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
[[fetch]]
|
||||
files = ["./antigravity.py"]
|
||||
|
||||
@@ -27,14 +27,14 @@
|
||||
</nav>
|
||||
<section class="pyscript">
|
||||
<h1 class="font-semibold text-2xl ml-5">Custom REPL</h1>
|
||||
<py-tutor modules="antigravity.py">
|
||||
<py-tutor modules="utils.py;antigravity.py">
|
||||
<py-config>
|
||||
packages = [
|
||||
"bokeh",
|
||||
"numpy"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
|
||||
[[fetch]]
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
id="run"
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
py-onClick="run()"
|
||||
py-click="run()"
|
||||
>
|
||||
Run!
|
||||
</button>
|
||||
@@ -86,7 +86,7 @@
|
||||
id="clear"
|
||||
type="button"
|
||||
class="button is-danger"
|
||||
py-onClick="clear()"
|
||||
py-click="clear()"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<py-tutor modules="utils.py">
|
||||
<py-config>
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
]
|
||||
[[fetch]]
|
||||
files = ["./utils.py"]
|
||||
|
||||
@@ -28,33 +28,30 @@
|
||||
</nav>
|
||||
<section class="pyscript">
|
||||
<h1>To Do List</h1>
|
||||
<py-tutor modules="utils.py;pylist.py">
|
||||
<py-register-widget
|
||||
src="./pylist.py"
|
||||
name="py-list"
|
||||
klass="PyList"
|
||||
></py-register-widget>
|
||||
|
||||
<py-tutor modules="utils.py">
|
||||
<py-config>
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py",
|
||||
"./py_list.py"
|
||||
]
|
||||
[[fetch]]
|
||||
files = ["./utils.py", "./pylist.py"]
|
||||
files = ["./utils.py"]
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
from js import document
|
||||
from datetime import datetime as dt
|
||||
from pyodide.ffi.wrappers import add_event_listener
|
||||
|
||||
def add_task(*ags, **kws):
|
||||
def add_task(*args, **kws):
|
||||
# create a new dictionary representing the new task
|
||||
new_task_content = Element("new-task-content")
|
||||
task = { "content": new_task_content.value, "done": False, "created_at": dt.now() }
|
||||
|
||||
# add a new task to the list and tell it to use the `content` key to show in the UI
|
||||
# and to use the key `done` to sync the task status with a checkbox element in the UI
|
||||
myList.add(task)
|
||||
myList = Element("myList")
|
||||
myList.element.pyElementInstance.add(task)
|
||||
|
||||
# clear the inputbox element used to create the new task
|
||||
new_task_content.clear()
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
<py-tutor modules="./utils.py;./todo.py">
|
||||
<py-config>
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py",
|
||||
"./py_list.py"
|
||||
]
|
||||
[[fetch]]
|
||||
files = ["./utils.py", "./todo.py"]
|
||||
|
||||
@@ -10,7 +10,7 @@ task_list = Element("list-tasks-container")
|
||||
new_task_content = Element("new-task-content")
|
||||
|
||||
|
||||
def add_task(*ags, **kws):
|
||||
def add_task(*args, **kws):
|
||||
# ignore empty task
|
||||
if not new_task_content.element.value:
|
||||
return None
|
||||
|
||||
@@ -6,13 +6,12 @@ build-backend = "setuptools.build_meta"
|
||||
dynamic = ["version"]
|
||||
|
||||
[tool.codespell]
|
||||
ignore-words-list = "afterall"
|
||||
skip = "pyscriptjs/node_modules/*,*.js,*.json"
|
||||
|
||||
[tool.ruff]
|
||||
builtins = [
|
||||
"Element",
|
||||
"PyItemTemplate",
|
||||
"PyListTemplate",
|
||||
"pyscript",
|
||||
]
|
||||
ignore = [
|
||||
|
||||
@@ -16,18 +16,33 @@ module.exports = {
|
||||
browser: true,
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
ignorePatterns: ['node_modules'],
|
||||
ignorePatterns: ['node_modules', 'src/interpreter_worker/*'],
|
||||
rules: {
|
||||
'no-prototype-builtins': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/restrict-plus-operands': 'warn',
|
||||
'@typescript-eslint/no-empty-function': 'warn',
|
||||
// ts-ignore is already an explicit override, no need to have a second lint
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
|
||||
// any related lints
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
|
||||
// other rules
|
||||
'no-prototype-builtins': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { args: 'none' }],
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/restrict-plus-operands': 'error',
|
||||
'@typescript-eslint/no-empty-function': 'error',
|
||||
'@typescript-eslint/restrict-template-expressions': ['error', { allowBoolean: true }],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['src/components/pyscript.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -58,6 +58,14 @@ dev:
|
||||
build:
|
||||
npm run build
|
||||
|
||||
build-fast:
|
||||
node esbuild.mjs
|
||||
|
||||
# use the following rule to do all the checks done by precommit: in
|
||||
# particular, use this if you want to run eslint.
|
||||
precommit-check:
|
||||
pre-commit run --all-files
|
||||
|
||||
examples:
|
||||
mkdir -p ./examples
|
||||
cp -r ../examples/* ./examples
|
||||
@@ -78,20 +86,26 @@ run-examples: setup build examples
|
||||
make dev
|
||||
|
||||
test:
|
||||
make examples
|
||||
make test-ts
|
||||
make test-py
|
||||
make test-integration-parallel
|
||||
make test-examples
|
||||
|
||||
# run all integration tests *including examples* sequentially
|
||||
test-integration:
|
||||
make examples
|
||||
mkdir -p test_results
|
||||
$(PYTEST_EXE) -vv $(ARGS) tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
||||
|
||||
# run all integration tests *except examples* in parallel (examples use too much memory)
|
||||
test-integration-parallel:
|
||||
mkdir -p test_results
|
||||
$(PYTEST_EXE) --numprocesses auto -vv $(ARGS) tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml -k 'not zz_examples'
|
||||
|
||||
# run integration tests on only examples sequentially (to avoid running out of memory)
|
||||
test-examples:
|
||||
make examples
|
||||
mkdir -p test_results
|
||||
$(PYTEST_EXE) --numprocesses auto -vv $(ARGS) tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
||||
$(PYTEST_EXE) -vv $(ARGS) tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml -k 'zz_examples'
|
||||
|
||||
test-py:
|
||||
@echo "Tests from $(src_dir)"
|
||||
|
||||
53
pyscriptjs/directoryManifest.mjs
Normal file
53
pyscriptjs/directoryManifest.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
// This logic split out because it is shared by:
|
||||
// 1. esbuild.mjs
|
||||
// 2. Jest setup.ts
|
||||
|
||||
import path, { join } from 'path';
|
||||
import { opendir, readFile } from 'fs/promises';
|
||||
|
||||
/**
|
||||
* List out everything in a directory, but skip __pycache__ directory. Used to
|
||||
* list out the directory paths and the [file path, file contents] pairs in the
|
||||
* Python package. All paths are relative to the directory we are listing. The
|
||||
* directories are sorted topologically so that a parent directory always
|
||||
* appears before its children.
|
||||
*
|
||||
* This is consumed in main.ts which calls mkdir for each directory and then
|
||||
* writeFile to create each file.
|
||||
*
|
||||
* @param {string} dir The path to the directory we want to list out
|
||||
* @returns {dirs: string[], files: [string, string][]}
|
||||
*/
|
||||
export async function directoryManifest(dir) {
|
||||
const result = { dirs: [], files: [] };
|
||||
await _directoryManifestHelper(dir, '.', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive helper function for directoryManifest
|
||||
*/
|
||||
async function _directoryManifestHelper(root, dir, result) {
|
||||
const dirObj = await opendir(join(root, dir));
|
||||
for await (const d of dirObj) {
|
||||
const entry = join(dir, d.name);
|
||||
if (d.isDirectory()) {
|
||||
if (d.name === '__pycache__') {
|
||||
continue;
|
||||
}
|
||||
result.dirs.push(entry);
|
||||
await _directoryManifestHelper(root, entry, result);
|
||||
} else if (d.isFile()) {
|
||||
result.files.push([normalizePath(entry), await readFile(join(root, entry), { encoding: 'utf-8' })]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize paths under different operating systems to
|
||||
* the correct path that will be used for src on browser.
|
||||
* @param {string} originalPath
|
||||
*/
|
||||
function normalizePath(originalPath) {
|
||||
return path.normalize(originalPath).replace(/\\/g, '/');
|
||||
}
|
||||
@@ -3,9 +3,9 @@ channels:
|
||||
- conda-forge
|
||||
- microsoft
|
||||
dependencies:
|
||||
- python=3.9
|
||||
- python=3.11.3
|
||||
- pip
|
||||
- pytest=7
|
||||
- pytest=7.1.2
|
||||
- nodejs=16
|
||||
- black
|
||||
- isort
|
||||
@@ -14,7 +14,12 @@ dependencies:
|
||||
- pillow
|
||||
- numpy
|
||||
- markdown
|
||||
- toml
|
||||
- pip:
|
||||
- playwright
|
||||
- pytest-playwright
|
||||
- pytest-xdist
|
||||
- playwright==1.33.0
|
||||
- pytest-playwright==0.3.3
|
||||
- pytest-xdist==3.3.0
|
||||
# We need Pyodide and micropip so we can import them in our Python
|
||||
# unit tests
|
||||
- pyodide_py==0.23.2
|
||||
- micropip==0.2.2
|
||||
|
||||
138
pyscriptjs/esbuild.mjs
Normal file
138
pyscriptjs/esbuild.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
import { build } from 'esbuild';
|
||||
import { spawn } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { watchFile } from 'fs';
|
||||
import { cp, lstat, readdir } from 'fs/promises';
|
||||
import { directoryManifest } from './directoryManifest.mjs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
const production = !process.env.NODE_WATCH || process.env.NODE_ENV === 'production';
|
||||
|
||||
const copy_targets = [
|
||||
{ src: 'public/index.html', dest: 'build' },
|
||||
{ src: 'src/plugins/python/*', dest: 'build/plugins/python' },
|
||||
];
|
||||
|
||||
if (!production) {
|
||||
copy_targets.push({ src: 'build/*', dest: 'examples/build' });
|
||||
}
|
||||
|
||||
/**
|
||||
* An esbuild plugin that injects the Pyscript Python package.
|
||||
*
|
||||
* It uses onResolve to attach our custom namespace to the import and then uses
|
||||
* onLoad to inject the file contents.
|
||||
*/
|
||||
function bundlePyscriptPythonPlugin() {
|
||||
const namespace = 'bundlePyscriptPythonPlugin';
|
||||
return {
|
||||
name: namespace,
|
||||
setup(build) {
|
||||
// Resolve the pyscript_package to our custom namespace
|
||||
// The path doesn't really matter, but we need a separate namespace
|
||||
// or else the file system resolver will raise an error.
|
||||
build.onResolve({ filter: /^pyscript_python_package.esbuild_injected.json$/ }, args => {
|
||||
return { path: 'dummy', namespace };
|
||||
});
|
||||
// Inject our manifest as JSON contents, and use the JSON loader.
|
||||
// Also tell esbuild to watch the files & directories we've listed
|
||||
// for updates.
|
||||
build.onLoad({ filter: /^dummy$/, namespace }, async args => {
|
||||
const manifest = await directoryManifest('./src/python');
|
||||
return {
|
||||
contents: JSON.stringify(manifest),
|
||||
loader: 'json',
|
||||
watchFiles: manifest.files.map(([k, v]) => k),
|
||||
watchDirs: manifest.dirs,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const pyScriptConfig = {
|
||||
entryPoints: ['src/main.ts'],
|
||||
loader: { '.py': 'text' },
|
||||
bundle: true,
|
||||
format: 'iife',
|
||||
globalName: 'pyscript',
|
||||
plugins: [bundlePyscriptPythonPlugin()],
|
||||
};
|
||||
|
||||
const interpreterWorkerConfig = {
|
||||
entryPoints: ['src/interpreter_worker/worker.ts'],
|
||||
loader: { '.py': 'text' },
|
||||
bundle: true,
|
||||
format: 'iife',
|
||||
plugins: [bundlePyscriptPythonPlugin()],
|
||||
};
|
||||
|
||||
const copyPath = (source, dest, ...rest) => cp(join(__dirname, source), join(__dirname, dest), ...rest);
|
||||
|
||||
const esbuild = async () => {
|
||||
const timer = `\x1b[1mpyscript\x1b[0m \x1b[2m(${production ? 'prod' : 'dev'})\x1b[0m built in`;
|
||||
console.time(timer);
|
||||
|
||||
await Promise.all([
|
||||
build({
|
||||
...pyScriptConfig,
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
outfile: 'build/pyscript.js',
|
||||
}),
|
||||
build({
|
||||
...pyScriptConfig,
|
||||
sourcemap: true,
|
||||
minify: true,
|
||||
outfile: 'build/pyscript.min.js',
|
||||
}),
|
||||
// XXX I suppose we should also build a minified version
|
||||
// TODO (HC): Simplify config a bit
|
||||
build({
|
||||
...interpreterWorkerConfig,
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
outfile: 'build/interpreter_worker.js',
|
||||
}),
|
||||
]);
|
||||
|
||||
const copy = [];
|
||||
for (const { src, dest } of copy_targets) {
|
||||
if (src.endsWith('/*')) {
|
||||
copy.push(copyPath(src.slice(0, -2), dest, { recursive: true }));
|
||||
} else {
|
||||
copy.push(copyPath(src, dest + src.slice(src.lastIndexOf('/'))));
|
||||
}
|
||||
}
|
||||
await Promise.all(copy);
|
||||
|
||||
console.timeEnd(timer);
|
||||
};
|
||||
|
||||
esbuild().then(() => {
|
||||
if (!production) {
|
||||
(async function watchPath(path) {
|
||||
for (const file of await readdir(path)) {
|
||||
const whole = join(path, file);
|
||||
if (/\.(js|ts|css|py)$/.test(file)) {
|
||||
watchFile(whole, async () => {
|
||||
await esbuild();
|
||||
});
|
||||
} else if ((await lstat(whole)).isDirectory()) {
|
||||
watchPath(whole);
|
||||
}
|
||||
}
|
||||
})('src');
|
||||
|
||||
const server = spawn('python', ['-m', 'http.server', '--directory', './examples', '8080'], {
|
||||
stdio: 'inherit',
|
||||
detached: false,
|
||||
});
|
||||
|
||||
process.on('exit', () => {
|
||||
server.kill();
|
||||
});
|
||||
}
|
||||
});
|
||||
28
pyscriptjs/jest-environment-jsdom.js
Normal file
28
pyscriptjs/jest-environment-jsdom.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const { TextEncoder, TextDecoder } = require('util');
|
||||
const { MessageChannel } = require('node:worker_threads');
|
||||
|
||||
const { default: $JSDOMEnvironment, TestEnvironment } = require('jest-environment-jsdom');
|
||||
|
||||
Object.defineProperty(exports, '__esModule', {
|
||||
value: true,
|
||||
});
|
||||
|
||||
class JSDOMEnvironment extends $JSDOMEnvironment {
|
||||
constructor(...args) {
|
||||
const { global } = super(...args);
|
||||
if (!global.TextEncoder) {
|
||||
global.TextEncoder = TextEncoder;
|
||||
}
|
||||
if (!global.TextDecoder) {
|
||||
global.TextDecoder = TextDecoder;
|
||||
}
|
||||
if (!global.MessageChannel) {
|
||||
global.MessageChannel = MessageChannel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.default = JSDOMEnvironment;
|
||||
exports.TestEnvironment = TestEnvironment === $JSDOMEnvironment ? JSDOMEnvironment : TestEnvironment;
|
||||
@@ -1,7 +1,8 @@
|
||||
//jest.config.js
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
setupFilesAfterEnv: ['./tests/unit/setup.ts'],
|
||||
testEnvironment: './jest-environment-jsdom.js',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
|
||||
5833
pyscriptjs/package-lock.json
generated
5833
pyscriptjs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,54 +2,46 @@
|
||||
"name": "pyscript",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build-min": "NODE_ENV=production rollup -c",
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"build": "npm run tsc && node esbuild.mjs",
|
||||
"dev": "NODE_WATCH=1 node esbuild.mjs",
|
||||
"tsc": "tsc --noEmit",
|
||||
"format:check": "prettier --check './src/**/*.{js,html,ts}'",
|
||||
"format": "prettier --write './src/**/*.{js,html,ts}'",
|
||||
"lint": "eslint './src/**/*.{js,html,ts}'",
|
||||
"lint:fix": "eslint --fix './src/**/*.{js,html,ts}'",
|
||||
"format:check": "prettier --check './src/**/*.{mjs,js,html,ts}'",
|
||||
"format": "prettier --write './src/**/*.{mjs,js,html,ts}'",
|
||||
"lint": "eslint './src/**/*.{mjs,js,html,ts}'",
|
||||
"lint:fix": "eslint --fix './src/**/*.{mjs,js,html,ts}'",
|
||||
"xprelint": "npm run format",
|
||||
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
||||
"test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.2.2",
|
||||
"@codemirror/lang-python": "^6.1.2",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.1",
|
||||
"@codemirror/view": "^6.9.3",
|
||||
"@hoodmane/toml-j0.4": "^1.1.2",
|
||||
"@jest/globals": "29.1.2",
|
||||
"@rollup/plugin-commonjs": "22.0.2",
|
||||
"@rollup/plugin-legacy": "2.2.0",
|
||||
"@rollup/plugin-node-resolve": "14.1.0",
|
||||
"@rollup/plugin-typescript": "8.5.0",
|
||||
"@types/codemirror": "^5.60.5",
|
||||
"@types/jest": "29.1.2",
|
||||
"@types/node": "18.8.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.39.0",
|
||||
"@typescript-eslint/parser": "5.39.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||
"@typescript-eslint/parser": "5.58.0",
|
||||
"codemirror": "6.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"esbuild": "0.17.12",
|
||||
"eslint": "8.25.0",
|
||||
"jest": "29.1.2",
|
||||
"jest-environment-jsdom": "29.1.2",
|
||||
"prettier": "2.7.1",
|
||||
"pyodide": "0.22.1",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-copy": "3.4.0",
|
||||
"rollup-plugin-css-only": "3.1.0",
|
||||
"rollup-plugin-livereload": "2.0.5",
|
||||
"rollup-plugin-serve": "2.0.1",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"ts-jest": "29.0.3",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.8.4"
|
||||
"pyodide": "0.23.2",
|
||||
"synclink": "0.2.4",
|
||||
"ts-jest": "29.1.0",
|
||||
"typescript": "5.0.4",
|
||||
"xterm": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "6.1.1",
|
||||
"@codemirror/lang-python": "6.0.2",
|
||||
"@codemirror/language": "6.2.1",
|
||||
"@codemirror/state": "6.1.2",
|
||||
"@codemirror/theme-one-dark": "6.1.0",
|
||||
"@codemirror/view": "6.3.0",
|
||||
"codemirror": "6.0.1",
|
||||
"toml-j0.4": "^1.1.1"
|
||||
"basic-devtools": "^0.1.6",
|
||||
"not-so-weak": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
import serve from 'rollup-plugin-serve';
|
||||
import { string } from 'rollup-plugin-string';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH || process.env.NODE_ENV === 'production';
|
||||
|
||||
const copy_targets = {
|
||||
targets: [
|
||||
{ src: 'public/index.html', dest: 'build' },
|
||||
{ src: 'src/plugins/*', dest: 'build/plugins' },
|
||||
],
|
||||
};
|
||||
|
||||
if (!production) {
|
||||
copy_targets.targets.push({ src: 'build/*', dest: 'examples/build' });
|
||||
}
|
||||
|
||||
export default {
|
||||
input: 'src/main.ts',
|
||||
output: [
|
||||
{
|
||||
file: 'build/pyscript.js',
|
||||
format: 'iife',
|
||||
sourcemap: true,
|
||||
inlineDynamicImports: true,
|
||||
name: 'pyscript',
|
||||
},
|
||||
{
|
||||
file: 'build/pyscript.min.js',
|
||||
format: 'iife',
|
||||
sourcemap: true,
|
||||
inlineDynamicImports: true,
|
||||
name: 'pyscript',
|
||||
plugins: [terser()],
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
css({ output: 'pyscript.css' }),
|
||||
// Bundle all the Python files into the output file
|
||||
string({
|
||||
include: './src/**/*.py',
|
||||
}),
|
||||
resolve({
|
||||
browser: true,
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({
|
||||
sourceMap: !production,
|
||||
inlineSources: !production,
|
||||
}),
|
||||
// This will make sure that examples will always get the latest build folder
|
||||
copy(copy_targets),
|
||||
// production && terser(),
|
||||
!production &&
|
||||
serve({
|
||||
port: 8080,
|
||||
contentBase: 'examples',
|
||||
}),
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
};
|
||||
@@ -1,15 +1,11 @@
|
||||
import { InterpreterClient } from '../interpreter_client';
|
||||
import type { PyScriptApp } from '../main';
|
||||
import { make_PyRepl } from './pyrepl';
|
||||
import { make_PyWidget } from './pywidget';
|
||||
|
||||
function createCustomElements(interpreter: InterpreterClient) {
|
||||
const PyWidget = make_PyWidget(interpreter);
|
||||
const PyRepl = make_PyRepl(interpreter);
|
||||
function createCustomElements(interpreter: InterpreterClient, app: PyScriptApp) {
|
||||
const PyRepl = make_PyRepl(interpreter, app);
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const xPyRepl = customElements.define('py-repl', PyRepl);
|
||||
const xPyWidget = customElements.define('py-register-widget', PyWidget);
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
customElements.define('py-repl', PyRepl);
|
||||
}
|
||||
|
||||
export { createCustomElements };
|
||||
|
||||
@@ -1,46 +1,43 @@
|
||||
import { $, $$ } from 'basic-devtools';
|
||||
|
||||
import { basicSetup, EditorView } from 'codemirror';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { indentUnit } from '@codemirror/language';
|
||||
import { Compartment } from '@codemirror/state';
|
||||
import { keymap } from '@codemirror/view';
|
||||
import { keymap, Command } from '@codemirror/view';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { oneDarkTheme } from '@codemirror/theme-one-dark';
|
||||
|
||||
import { getAttribute, ensureUniqueId, htmlDecode } from '../utils';
|
||||
import { pyExec, pyDisplay } from '../pyexec';
|
||||
import { ensureUniqueId, htmlDecode } from '../utils';
|
||||
import { pyExec } from '../pyexec';
|
||||
import { getLogger } from '../logger';
|
||||
import { InterpreterClient } from '../interpreter_client';
|
||||
import type { PyScriptApp } from '../main';
|
||||
import { Stdio } from '../stdio';
|
||||
import { robustFetch } from '../fetch';
|
||||
import { _createAlertBanner } from '../exceptions';
|
||||
|
||||
const logger = getLogger('py-repl');
|
||||
const RUNBUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
||||
|
||||
export function make_PyRepl(interpreter: InterpreterClient) {
|
||||
export function make_PyRepl(interpreter: InterpreterClient, app: PyScriptApp) {
|
||||
/* High level structure of py-repl DOM, and the corresponding JS names.
|
||||
|
||||
this <py-repl>
|
||||
shadow #shadow-root
|
||||
<slot></slot>
|
||||
boxDiv <div class='py-repl-box'>
|
||||
editorLabel <label>...</label>
|
||||
editorDiv <div class="py-repl-editor"></div>
|
||||
outDiv <div class="py-repl-output"></div>
|
||||
</div>
|
||||
</py-repl>
|
||||
*/
|
||||
class PyRepl extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
outDiv: HTMLElement;
|
||||
editor: EditorView;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
stdout_manager: Stdio | null;
|
||||
stderr_manager: Stdio | null;
|
||||
static observedAttributes = ['src'];
|
||||
connectedCallback() {
|
||||
ensureUniqueId(this);
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
const slot = document.createElement('slot');
|
||||
this.shadow.appendChild(slot);
|
||||
|
||||
if (!this.hasAttribute('exec-id')) {
|
||||
this.setAttribute('exec-id', '0');
|
||||
@@ -51,16 +48,55 @@ export function make_PyRepl(interpreter: InterpreterClient) {
|
||||
|
||||
const pySrc = htmlDecode(this.innerHTML).trim();
|
||||
this.innerHTML = '';
|
||||
this.editor = this.makeEditor(pySrc);
|
||||
const boxDiv = this.makeBoxDiv();
|
||||
const shadowRoot = $('.py-repl-editor > div', boxDiv).attachShadow({ mode: 'open' });
|
||||
// avoid inheriting styles from the outer component
|
||||
shadowRoot.innerHTML = `<style> :host { all: initial; }</style>`;
|
||||
this.appendChild(boxDiv);
|
||||
this.editor = this.makeEditor(pySrc, shadowRoot);
|
||||
this.editor.focus();
|
||||
logger.debug(`element ${this.id} successfully connected`);
|
||||
}
|
||||
|
||||
get src() {
|
||||
return this.getAttribute('src');
|
||||
}
|
||||
|
||||
set src(value) {
|
||||
this.setAttribute('src', value);
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, oldVal: string, newVal: string) {
|
||||
if (name === 'src' && newVal !== oldVal) {
|
||||
void this.loadReplSrc();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch url from src attribute of py-repl tags and
|
||||
* preload the code from fetch response into the Corresponding py-repl tag,
|
||||
* but please note that they will not be pre-run unless you click the runbotton.
|
||||
*/
|
||||
async loadReplSrc() {
|
||||
try {
|
||||
const response = await robustFetch(this.src);
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
const cmcontentElement = $('div.cm-content', this.editor.dom);
|
||||
const { lastElementChild } = cmcontentElement;
|
||||
cmcontentElement.replaceChildren(lastElementChild);
|
||||
lastElementChild.textContent = await response.text();
|
||||
logger.info(`loading code from ${this.src} to repl...success`);
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
_createAlertBanner(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create and configure the codemirror editor
|
||||
*/
|
||||
makeEditor(pySrc: string): EditorView {
|
||||
makeEditor(pySrc: string, parent: ShadowRoot): EditorView {
|
||||
const languageConf = new Compartment();
|
||||
const extensions = [
|
||||
indentUnit.of(' '),
|
||||
@@ -68,18 +104,19 @@ export function make_PyRepl(interpreter: InterpreterClient) {
|
||||
languageConf.of(python()),
|
||||
keymap.of([
|
||||
...defaultKeymap,
|
||||
{ key: 'Ctrl-Enter', run: this.execute.bind(this), preventDefault: true },
|
||||
{ key: 'Shift-Enter', run: this.execute.bind(this), preventDefault: true },
|
||||
{ key: 'Ctrl-Enter', run: this.execute.bind(this) as Command, preventDefault: true },
|
||||
{ key: 'Shift-Enter', run: this.execute.bind(this) as Command, preventDefault: true },
|
||||
]),
|
||||
];
|
||||
|
||||
if (getAttribute(this, 'theme') === 'dark') {
|
||||
if (this.getAttribute('theme') === 'dark') {
|
||||
extensions.push(oneDarkTheme);
|
||||
}
|
||||
|
||||
return new EditorView({
|
||||
doc: pySrc,
|
||||
extensions,
|
||||
parent,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,10 +130,8 @@ export function make_PyRepl(interpreter: InterpreterClient) {
|
||||
boxDiv.className = 'py-repl-box';
|
||||
|
||||
const editorDiv = this.makeEditorDiv();
|
||||
const editorLabel = this.makeLabel('Python Script Area', editorDiv);
|
||||
this.outDiv = this.makeOutDiv();
|
||||
|
||||
boxDiv.append(editorLabel);
|
||||
boxDiv.appendChild(editorDiv);
|
||||
boxDiv.appendChild(this.outDiv);
|
||||
|
||||
@@ -105,36 +140,28 @@ export function make_PyRepl(interpreter: InterpreterClient) {
|
||||
|
||||
makeEditorDiv(): HTMLElement {
|
||||
const editorDiv = document.createElement('div');
|
||||
editorDiv.id = 'code-editor';
|
||||
editorDiv.className = 'py-repl-editor';
|
||||
editorDiv.appendChild(this.editor.dom);
|
||||
editorDiv.setAttribute('aria-label', 'Python Script Area');
|
||||
|
||||
const runButton = this.makeRunButton();
|
||||
const runLabel = this.makeLabel('Python Script Run Button', runButton);
|
||||
editorDiv.appendChild(runLabel);
|
||||
editorDiv.appendChild(runButton);
|
||||
const editorShadowContainer = document.createElement('div');
|
||||
|
||||
// avoid outer elements intercepting key events (reveal as example)
|
||||
editorShadowContainer.addEventListener('keydown', event => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
editorDiv.append(editorShadowContainer, runButton);
|
||||
|
||||
return editorDiv;
|
||||
}
|
||||
|
||||
makeLabel(text: string, elementFor: HTMLElement): HTMLElement {
|
||||
ensureUniqueId(elementFor);
|
||||
const lbl = document.createElement('label');
|
||||
lbl.innerHTML = text;
|
||||
lbl.htmlFor = elementFor.id;
|
||||
// XXX this should be a CSS class
|
||||
// Styles that we use to hide the labels whilst also keeping it accessible for screen readers
|
||||
const labelStyle = 'overflow:hidden; display:block; width:1px; height:1px';
|
||||
lbl.setAttribute('style', labelStyle);
|
||||
return lbl;
|
||||
}
|
||||
|
||||
makeRunButton(): HTMLElement {
|
||||
const runButton = document.createElement('button');
|
||||
runButton.id = 'runButton';
|
||||
runButton.className = 'absolute py-repl-run-button';
|
||||
runButton.innerHTML = RUNBUTTON;
|
||||
runButton.addEventListener('click', this.execute.bind(this));
|
||||
runButton.setAttribute('aria-label', 'Python Script Run Button');
|
||||
runButton.addEventListener('click', this.execute.bind(this) as (e: MouseEvent) => void);
|
||||
return runButton;
|
||||
}
|
||||
|
||||
@@ -152,27 +179,19 @@ export function make_PyRepl(interpreter: InterpreterClient) {
|
||||
*/
|
||||
async execute(): Promise<void> {
|
||||
const pySrc = this.getPySrc();
|
||||
|
||||
// determine the output element
|
||||
const outEl = this.getOutputElement();
|
||||
if (outEl === undefined) {
|
||||
// this happens if we specified output="..." but we couldn't
|
||||
// find the ID. We already displayed an error message inside
|
||||
// getOutputElement, stop the execution.
|
||||
return;
|
||||
}
|
||||
|
||||
// clear the old output before executing the new code
|
||||
outEl.innerHTML = '';
|
||||
const outEl = this.outDiv;
|
||||
|
||||
// execute the python code
|
||||
const pyResult = (await pyExec(interpreter, pySrc, outEl)).result;
|
||||
|
||||
// display the value of the last evaluated expression (REPL-style)
|
||||
if (pyResult !== undefined) {
|
||||
pyDisplay(interpreter, pyResult, { target: outEl.id });
|
||||
}
|
||||
|
||||
await app.plugins.beforePyReplExec({ interpreter: interpreter, src: pySrc, outEl: outEl, pyReplTag: this });
|
||||
const { result } = await pyExec(interpreter, pySrc, outEl);
|
||||
await app.plugins.afterPyReplExec({
|
||||
interpreter: interpreter,
|
||||
src: pySrc,
|
||||
outEl: outEl,
|
||||
pyReplTag: this,
|
||||
result,
|
||||
});
|
||||
await interpreter._remote.destroyIfProxy(result);
|
||||
this.autogenerateMaybe();
|
||||
}
|
||||
|
||||
@@ -180,32 +199,25 @@ export function make_PyRepl(interpreter: InterpreterClient) {
|
||||
return this.editor.state.doc.toString();
|
||||
}
|
||||
|
||||
getOutputElement(): HTMLElement {
|
||||
const outputID = getAttribute(this, 'output');
|
||||
if (outputID !== null) {
|
||||
const el = document.getElementById(outputID);
|
||||
if (el === null) {
|
||||
const err = `py-repl ERROR: cannot find the output element #${outputID} in the DOM`;
|
||||
this.outDiv.innerText = err;
|
||||
return undefined;
|
||||
}
|
||||
return el;
|
||||
} else {
|
||||
return this.outDiv;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX the autogenerate logic is very messy. We should redo it, and it
|
||||
// should be the default.
|
||||
autogenerateMaybe(): void {
|
||||
if (this.hasAttribute('auto-generate')) {
|
||||
const allPyRepls = document.querySelectorAll(`py-repl[root='${this.getAttribute('root')}'][exec-id]`);
|
||||
const allPyRepls = $$(`py-repl[root='${this.getAttribute('root')}'][exec-id]`, document);
|
||||
const lastRepl = allPyRepls[allPyRepls.length - 1];
|
||||
const lastExecId = lastRepl.getAttribute('exec-id');
|
||||
const nextExecId = parseInt(lastExecId) + 1;
|
||||
|
||||
const newPyRepl = document.createElement('py-repl');
|
||||
newPyRepl.setAttribute('root', this.getAttribute('root'));
|
||||
|
||||
//Attributes to be copied from old REPL to auto-generated REPL
|
||||
for (const attribute of ['root', 'output-mode', 'output', 'stderr']) {
|
||||
const attr = this.getAttribute(attribute);
|
||||
if (attr) {
|
||||
newPyRepl.setAttribute(attribute, attr);
|
||||
}
|
||||
}
|
||||
|
||||
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();
|
||||
|
||||
if (this.hasAttribute('auto-generate')) {
|
||||
@@ -213,20 +225,6 @@ export function make_PyRepl(interpreter: InterpreterClient) {
|
||||
this.removeAttribute('auto-generate');
|
||||
}
|
||||
|
||||
const outputMode = getAttribute(this, 'output-mode');
|
||||
if (outputMode) {
|
||||
newPyRepl.setAttribute('output-mode', outputMode);
|
||||
}
|
||||
|
||||
const addReplAttribute = (attribute: string) => {
|
||||
const attr = getAttribute(this, attribute);
|
||||
if (attr) {
|
||||
newPyRepl.setAttribute(attribute, attr);
|
||||
}
|
||||
};
|
||||
|
||||
addReplAttribute('output');
|
||||
|
||||
newPyRepl.setAttribute('exec-id', nextExecId.toString());
|
||||
if (this.parentElement) {
|
||||
this.parentElement.appendChild(newPyRepl);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { htmlDecode, ensureUniqueId, createDeprecationWarning } from '../utils';
|
||||
import { $$, $x } from 'basic-devtools';
|
||||
|
||||
import { shadowRoots } from '../shadow_roots';
|
||||
import { ltrim, htmlDecode, ensureUniqueId, createDeprecationWarning } from '../utils';
|
||||
import { getLogger } from '../logger';
|
||||
import { pyExec, displayPyException } from '../pyexec';
|
||||
import { _createAlertBanner } from '../exceptions';
|
||||
@@ -9,13 +12,14 @@ import { InterpreterClient } from '../interpreter_client';
|
||||
|
||||
const logger = getLogger('py-script');
|
||||
|
||||
export function make_PyScript(interpreter: InterpreterClient, app: PyScriptApp) {
|
||||
class PyScript extends HTMLElement {
|
||||
srcCode: string;
|
||||
stdout_manager: Stdio | null;
|
||||
stderr_manager: Stdio | null;
|
||||
// used to flag already initialized nodes
|
||||
const knownPyScriptTags: WeakSet<HTMLElement> = new WeakSet();
|
||||
|
||||
async connectedCallback() {
|
||||
export function make_PyScript(interpreter: InterpreterClient, app: PyScriptApp) {
|
||||
/**
|
||||
* A common <py-script> VS <script type="py"> initializator.
|
||||
*/
|
||||
const init = async (pyScriptTag: PyScript, fallback: () => string) => {
|
||||
/**
|
||||
* Since connectedCallback is async, multiple py-script tags can be executed in
|
||||
* an order which is not particularly sequential. The locking mechanism here ensures
|
||||
@@ -23,214 +27,232 @@ export function make_PyScript(interpreter: InterpreterClient, app: PyScriptApp)
|
||||
*
|
||||
* Concurrent access to the multiple py-script tags is thus avoided.
|
||||
*/
|
||||
let releaseLock: any;
|
||||
app.incrementPendingTags();
|
||||
let releaseLock: () => void;
|
||||
try {
|
||||
releaseLock = await app.tagExecutionLock();
|
||||
ensureUniqueId(this);
|
||||
ensureUniqueId(pyScriptTag);
|
||||
const src = await fetchSource(pyScriptTag, fallback);
|
||||
await app.plugins.beforePyScriptExec({ interpreter, src, pyScriptTag });
|
||||
const { result } = await pyExec(interpreter, src, pyScriptTag);
|
||||
await app.plugins.afterPyScriptExec({ interpreter, src, pyScriptTag, result });
|
||||
await interpreter._remote.destroyIfProxy(result);
|
||||
} finally {
|
||||
releaseLock();
|
||||
app.decrementPendingTags();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
|
||||
* It either throws an error if the 'src' can't be fetched or it returns a fallback
|
||||
* content as source.
|
||||
*/
|
||||
const fetchSource = async (tag: Element, fallback: () => string): Promise<string> => {
|
||||
if (tag.hasAttribute('src')) {
|
||||
try {
|
||||
const response = await robustFetch(tag.getAttribute('src'));
|
||||
return await response.text();
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
_createAlertBanner(e.message);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return fallback();
|
||||
};
|
||||
|
||||
class PyScript extends HTMLElement {
|
||||
srcCode: string;
|
||||
stdout_manager: Stdio | null;
|
||||
stderr_manager: Stdio | null;
|
||||
_fetchSourceFallback = () => htmlDecode(this.srcCode);
|
||||
|
||||
async connectedCallback() {
|
||||
// prevent multiple initialization of the same node if re-appended
|
||||
if (knownPyScriptTags.has(this)) return;
|
||||
knownPyScriptTags.add(this);
|
||||
|
||||
// Save innerHTML information in srcCode so we can access it later
|
||||
// once we clean innerHTML (which is required since we don't want
|
||||
// source code to be rendered on the screen)
|
||||
this.srcCode = this.innerHTML;
|
||||
const pySrc = await this.getPySrc();
|
||||
this.innerHTML = '';
|
||||
await init(this, this._fetchSourceFallback);
|
||||
}
|
||||
|
||||
app.plugins.beforePyScriptExec({ interpreter: interpreter, src: pySrc, pyScriptTag: this });
|
||||
const result = (await pyExec(interpreter, pySrc, this)).result;
|
||||
app.plugins.afterPyScriptExec({
|
||||
interpreter: interpreter,
|
||||
src: pySrc,
|
||||
pyScriptTag: this,
|
||||
result: result,
|
||||
});
|
||||
} finally {
|
||||
releaseLock();
|
||||
getPySrc(): Promise<string> {
|
||||
return fetchSource(this, this._fetchSourceFallback);
|
||||
}
|
||||
}
|
||||
|
||||
async getPySrc(): Promise<string> {
|
||||
if (this.hasAttribute('src')) {
|
||||
const url = this.getAttribute('src');
|
||||
try {
|
||||
const response = await robustFetch(url);
|
||||
return await response.text();
|
||||
} catch (e) {
|
||||
_createAlertBanner(e.message);
|
||||
this.innerHTML = '';
|
||||
throw e;
|
||||
// bootstrap the <script> tag fallback only if needed (once per definition)
|
||||
if (!customElements.get('py-script')) {
|
||||
// allow any HTMLScriptElement to behave like a PyScript custom-elelement
|
||||
type PyScriptElement = HTMLScriptElement & PyScript;
|
||||
|
||||
// the <script> tags to look for, acting like a <py-script> one
|
||||
// both py, pyscript, and py-script, are valid types to help reducing typo cases
|
||||
const pyScriptCSS = 'script[type="py"],script[type="pyscript"],script[type="py-script"]';
|
||||
|
||||
// bootstrap with the same connectedCallback logic any <script>
|
||||
const bootstrap = (script: PyScriptElement) => {
|
||||
// prevent multiple initialization of the same node if re-appended
|
||||
if (knownPyScriptTags.has(script)) return;
|
||||
knownPyScriptTags.add(script);
|
||||
|
||||
const pyScriptTag = document.createElement('py-script-tag') as PyScript;
|
||||
|
||||
// move attributes to the live resulting pyScriptTag reference
|
||||
for (const name of ['output', 'src', 'stderr']) {
|
||||
const value = script.getAttribute(name);
|
||||
if (value) {
|
||||
pyScriptTag.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
// insert pyScriptTag companion right after the original script
|
||||
script.after(pyScriptTag);
|
||||
|
||||
// remove the first empty line to preserve line numbers/counting
|
||||
init(pyScriptTag, () => ltrim(script.textContent.replace(/^[\r\n]+/, ''))).catch(() =>
|
||||
pyScriptTag.remove(),
|
||||
);
|
||||
};
|
||||
|
||||
// loop over all py scripts and botstrap these
|
||||
const bootstrapScripts = (root: Document | Element) => {
|
||||
for (const node of $$(pyScriptCSS, root)) {
|
||||
bootstrap(node as PyScriptElement);
|
||||
}
|
||||
};
|
||||
|
||||
// globally shared MutationObserver for <script> special cases
|
||||
const pyScriptMO = new MutationObserver(records => {
|
||||
for (const { type, target, attributeName, addedNodes } of records) {
|
||||
if (type === 'attributes') {
|
||||
// consider only py-* attributes
|
||||
if (attributeName.startsWith('py-')) {
|
||||
// if the attribute is currently present
|
||||
if ((target as Element).hasAttribute(attributeName)) {
|
||||
// handle the element
|
||||
addPyScriptEventListener(
|
||||
getInterpreter(target as Element),
|
||||
target as Element,
|
||||
attributeName.slice(3),
|
||||
);
|
||||
} else {
|
||||
return htmlDecode(this.srcCode);
|
||||
// remove the listener because the element should not answer
|
||||
// to this specific event anymore
|
||||
|
||||
// Note: this is *NOT* a misused-promise, this is how async events work.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
target.removeEventListener(attributeName.slice(3), pyScriptListener);
|
||||
}
|
||||
}
|
||||
// skip further loop on empty addedNodes
|
||||
continue;
|
||||
}
|
||||
for (const node of addedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
if ((node as PyScriptElement).matches(pyScriptCSS)) {
|
||||
bootstrap(node as PyScriptElement);
|
||||
} else {
|
||||
addAllPyScriptEventListeners(node as Element);
|
||||
bootstrapScripts(node as Element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// simplifies observing any root node (document/shadowRoot)
|
||||
const observe = (root: Document | ShadowRoot) => {
|
||||
pyScriptMO.observe(root, { childList: true, subtree: true, attributes: true });
|
||||
return root;
|
||||
};
|
||||
|
||||
// patch attachShadow once to bootstrap <script> special cases in there too
|
||||
const { attachShadow } = Element.prototype;
|
||||
Object.assign(Element.prototype, {
|
||||
attachShadow(init: ShadowRootInit) {
|
||||
const shadowRoot = observe(attachShadow.call(this as Element, init));
|
||||
shadowRoots.add(shadowRoot);
|
||||
return shadowRoot;
|
||||
},
|
||||
});
|
||||
|
||||
// bootstrap all already live py <script> tags
|
||||
bootstrapScripts(document);
|
||||
|
||||
// once all tags have been initialized, observe new possible tags added later on
|
||||
// this is to save a few ticks within the callback as each <script> already adds a companion node
|
||||
observe(document);
|
||||
}
|
||||
|
||||
return PyScript;
|
||||
}
|
||||
|
||||
/** Defines all possible py-on* and their corresponding event types */
|
||||
const pyAttributeToEvent: Map<string, string> = new Map<string, string>([
|
||||
// Leaving pys-onClick and pys-onKeyDown for backward compatibility
|
||||
['pys-onClick', 'click'],
|
||||
['pys-onKeyDown', 'keydown'],
|
||||
['py-onClick', 'click'],
|
||||
['py-onKeyDown', 'keydown'],
|
||||
// Window Events
|
||||
['py-afterprint', 'afterprint'],
|
||||
['py-beforeprint', 'beforeprint'],
|
||||
['py-beforeunload', 'beforeunload'],
|
||||
['py-error', 'error'],
|
||||
['py-hashchange', 'hashchange'],
|
||||
['py-load', 'load'],
|
||||
['py-message', 'message'],
|
||||
['py-offline', 'offline'],
|
||||
['py-online', 'online'],
|
||||
['py-pagehide', 'pagehide'],
|
||||
['py-pageshow', 'pageshow'],
|
||||
['py-popstate', 'popstate'],
|
||||
['py-resize', 'resize'],
|
||||
['py-storage', 'storage'],
|
||||
['py-unload', 'unload'],
|
||||
/** A weak relation between an element and current interpreter */
|
||||
const elementInterpreter: WeakMap<Element, InterpreterClient> = new WeakMap();
|
||||
|
||||
// Form Events
|
||||
['py-blur', 'blur'],
|
||||
['py-change', 'change'],
|
||||
['py-contextmenu', 'contextmenu'],
|
||||
['py-focus', 'focus'],
|
||||
['py-input', 'input'],
|
||||
['py-invalid', 'invalid'],
|
||||
['py-reset', 'reset'],
|
||||
['py-search', 'search'],
|
||||
['py-select', 'select'],
|
||||
['py-submit', 'submit'],
|
||||
/** Return the interpreter, if any, or vallback to the last known one */
|
||||
const getInterpreter = (el: Element) => elementInterpreter.get(el) || lastInterpreter;
|
||||
|
||||
// Keyboard Events
|
||||
['py-keydown', 'keydown'],
|
||||
['py-keypress', 'keypress'],
|
||||
['py-keyup', 'keyup'],
|
||||
/** Retain last used interpreter to bootstrap PyScript to augment via MO runtime nodes */
|
||||
let lastInterpreter: InterpreterClient;
|
||||
|
||||
// Mouse Events
|
||||
['py-click', 'click'],
|
||||
['py-dblclick', 'dblclick'],
|
||||
['py-mousedown', 'mousedown'],
|
||||
['py-mousemove', 'mousemove'],
|
||||
['py-mouseout', 'mouseout'],
|
||||
['py-mouseover', 'mouseover'],
|
||||
['py-mouseup', 'mouseup'],
|
||||
['py-mousewheel', 'mousewheel'],
|
||||
['py-wheel', 'wheel'],
|
||||
|
||||
// Drag Events
|
||||
['py-drag', 'drag'],
|
||||
['py-dragend', 'dragend'],
|
||||
['py-dragenter', 'dragenter'],
|
||||
['py-dragleave', 'dragleave'],
|
||||
['py-dragover', 'dragover'],
|
||||
['py-dragstart', 'dragstart'],
|
||||
['py-drop', 'drop'],
|
||||
['py-scroll', 'scroll'],
|
||||
|
||||
// Clipboard Events
|
||||
['py-copy', 'copy'],
|
||||
['py-cut', 'cut'],
|
||||
['py-paste', 'paste'],
|
||||
|
||||
// Media Events
|
||||
['py-abort', 'abort'],
|
||||
['py-canplay', 'canplay'],
|
||||
['py-canplaythrough', 'canplaythrough'],
|
||||
['py-cuechange', 'cuechange'],
|
||||
['py-durationchange', 'durationchange'],
|
||||
['py-emptied', 'emptied'],
|
||||
['py-ended', 'ended'],
|
||||
['py-loadeddata', 'loadeddata'],
|
||||
['py-loadedmetadata', 'loadedmetadata'],
|
||||
['py-loadstart', 'loadstart'],
|
||||
['py-pause', 'pause'],
|
||||
['py-play', 'play'],
|
||||
['py-playing', 'playing'],
|
||||
['py-progress', 'progress'],
|
||||
['py-ratechange', 'ratechange'],
|
||||
['py-seeked', 'seeked'],
|
||||
['py-seeking', 'seeking'],
|
||||
['py-stalled', 'stalled'],
|
||||
['py-suspend', 'suspend'],
|
||||
['py-timeupdate', 'timeupdate'],
|
||||
['py-volumechange', 'volumechange'],
|
||||
['py-waiting', 'waiting'],
|
||||
|
||||
// Misc Events
|
||||
['py-toggle', 'toggle'],
|
||||
]);
|
||||
/** Find all py-* attributes in a context node and its descendant + add listeners */
|
||||
const addAllPyScriptEventListeners = (root: Document | Element) => {
|
||||
// note the XPath needs to start with a `.` to reference the starting root element
|
||||
const attributes = $x('.//@*[starts-with(name(), "py-")]', root) as Attr[];
|
||||
for (const { name, ownerElement: el } of attributes) {
|
||||
addPyScriptEventListener(getInterpreter(el), el, name.slice(3));
|
||||
}
|
||||
};
|
||||
|
||||
/** Initialize all elements with py-* handlers attributes */
|
||||
export function initHandlers(interpreter: InterpreterClient) {
|
||||
logger.debug('Initializing py-* event handlers...');
|
||||
for (const pyAttribute of pyAttributeToEvent.keys()) {
|
||||
createElementsWithEventListeners(interpreter, pyAttribute);
|
||||
}
|
||||
lastInterpreter = interpreter;
|
||||
addAllPyScriptEventListeners(document);
|
||||
}
|
||||
|
||||
/** Initializes an element with the given py-on* attribute and its handler */
|
||||
function createElementsWithEventListeners(interpreter: InterpreterClient, pyAttribute: string) {
|
||||
const matches: NodeListOf<HTMLElement> = document.querySelectorAll(`[${pyAttribute}]`);
|
||||
for (const el of matches) {
|
||||
// If the element doesn't have an id, let's add one automatically!
|
||||
if (el.id.length === 0) {
|
||||
ensureUniqueId(el);
|
||||
}
|
||||
const handlerCode = el.getAttribute(pyAttribute);
|
||||
const event = pyAttributeToEvent.get(pyAttribute);
|
||||
|
||||
if (pyAttribute === 'pys-onClick' || pyAttribute === 'pys-onKeyDown') {
|
||||
const msg =
|
||||
`The attribute 'pys-onClick' and 'pys-onKeyDown' are deprecated. Please 'py-click="myFunction()"' ` +
|
||||
` or 'py-keydown="myFunction()"' instead.`;
|
||||
createDeprecationWarning(msg, msg);
|
||||
const source = `
|
||||
from pyodide.ffi import create_proxy
|
||||
Element("${el.id}").element.addEventListener("${event}", create_proxy(${handlerCode}))
|
||||
`;
|
||||
|
||||
// We meed to run the source code in a try/catch block, because
|
||||
// the source code may contain a syntax error, which will cause
|
||||
// the splashscreen to not be removed.
|
||||
/** An always same listeners to reduce RAM and enable future runtime changes via MO */
|
||||
const pyScriptListener = async ({ type, currentTarget: el }) => {
|
||||
try {
|
||||
interpreter.run(source);
|
||||
const interpreter = getInterpreter(el);
|
||||
await interpreter.run(el.getAttribute(`py-${type as string}`));
|
||||
} catch (e) {
|
||||
logger.error((e as Error).message);
|
||||
}
|
||||
} else {
|
||||
el.addEventListener(event, () => {
|
||||
void (async () => {
|
||||
try {
|
||||
await interpreter.run(handlerCode);
|
||||
} catch (err) {
|
||||
const err = e as Error;
|
||||
displayPyException(err, el.parentElement);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
// TODO: Should we actually map handlers in JS instead of Python?
|
||||
// el.onclick = (evt: any) => {
|
||||
// console.log("click");
|
||||
// new Promise((resolve, reject) => {
|
||||
// setTimeout(() => {
|
||||
// console.log('Inside')
|
||||
// }, 300);
|
||||
// }).then(() => {
|
||||
// console.log("resolved")
|
||||
// });
|
||||
// // let handlerCode = el.getAttribute('py-onClick');
|
||||
// // pyodide.runPython(handlerCode);
|
||||
// }
|
||||
};
|
||||
|
||||
/** Weakly relate an element with an interpreter and then add the listener's type */
|
||||
function addPyScriptEventListener(interpreter: InterpreterClient, el: Element, type: string) {
|
||||
// If the element doesn't have an id, let's add one automatically!
|
||||
if (el.id.length === 0) {
|
||||
ensureUniqueId(el as HTMLElement);
|
||||
}
|
||||
elementInterpreter.set(el, interpreter);
|
||||
// Note: this is *NOT* a misused-promise, this is how async events work.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
el.addEventListener(type, pyScriptListener);
|
||||
}
|
||||
|
||||
/** Mount all elements with attribute py-mount into the Python namespace */
|
||||
export async function mountElements(interpreter: InterpreterClient) {
|
||||
const matches: NodeListOf<HTMLElement> = document.querySelectorAll('[py-mount]');
|
||||
const matches = $$('[py-mount]', document);
|
||||
logger.info(`py-mount: found ${matches.length} elements`);
|
||||
|
||||
if (matches.length > 0) {
|
||||
//last non-deprecated version: 2023.03.1
|
||||
const deprecationMessage =
|
||||
'The "py-mount" attribute is deprecated. Please add references to HTML Elements manually in your script.';
|
||||
createDeprecationWarning(deprecationMessage, 'py-mount');
|
||||
}
|
||||
|
||||
let source = '';
|
||||
for (const el of matches) {
|
||||
const mountName = el.getAttribute('py-mount') || el.id.split('-').join('_');
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import type { PyProxy } from 'pyodide';
|
||||
import { getLogger } from '../logger';
|
||||
import { robustFetch } from '../fetch';
|
||||
import { InterpreterClient } from '../interpreter_client';
|
||||
|
||||
const logger = getLogger('py-register-widget');
|
||||
|
||||
function createWidget(interpreter: InterpreterClient, name: string, code: string, klass: string) {
|
||||
class CustomWidget extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
wrapper: HTMLElement;
|
||||
|
||||
name: string = name;
|
||||
klass: string = klass;
|
||||
code: string = code;
|
||||
proxy: PyProxy;
|
||||
proxyClass: any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// attach shadow so we can preserve the element original innerHtml content
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.wrapper = document.createElement('slot');
|
||||
this.shadow.appendChild(this.wrapper);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await interpreter.runButDontRaise(this.code);
|
||||
this.proxyClass = interpreter.globals.get(this.klass);
|
||||
this.proxy = this.proxyClass(this);
|
||||
this.proxy.connect();
|
||||
this.registerWidget();
|
||||
}
|
||||
|
||||
registerWidget() {
|
||||
logger.info('new widget registered:', this.name);
|
||||
interpreter.globals.set(this.id, this.proxy);
|
||||
}
|
||||
}
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const xPyWidget = customElements.define(name, CustomWidget);
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
}
|
||||
|
||||
export function make_PyWidget(interpreter: InterpreterClient) {
|
||||
class PyWidget extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
name: string;
|
||||
klass: string;
|
||||
outputElement: HTMLElement;
|
||||
errorElement: HTMLElement;
|
||||
wrapper: HTMLElement;
|
||||
theme: string;
|
||||
source: string;
|
||||
code: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// attach shadow so we can preserve the element original innerHtml content
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.wrapper = document.createElement('slot');
|
||||
this.shadow.appendChild(this.wrapper);
|
||||
|
||||
this.addAttributes('src', 'name', 'klass');
|
||||
}
|
||||
|
||||
addAttributes(...attrs: string[]) {
|
||||
for (const each of attrs) {
|
||||
const property = each === 'src' ? 'source' : each;
|
||||
if (this.hasAttribute(each)) {
|
||||
this[property] = this.getAttribute(each);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
if (this.id === undefined) {
|
||||
throw new ReferenceError(
|
||||
`No id specified for component. Components must have an explicit id. Please use id="" to specify your component id.`,
|
||||
);
|
||||
}
|
||||
|
||||
const mainDiv = document.createElement('div');
|
||||
mainDiv.id = this.id + '-main';
|
||||
this.appendChild(mainDiv);
|
||||
logger.debug('PyWidget: reading source', this.source);
|
||||
this.code = await this.getSourceFromFile(this.source);
|
||||
createWidget(interpreter, this.name, this.code, this.klass);
|
||||
}
|
||||
|
||||
async getSourceFromFile(s: string): Promise<string> {
|
||||
const response = await robustFetch(s);
|
||||
return await response.text();
|
||||
}
|
||||
}
|
||||
|
||||
return PyWidget;
|
||||
}
|
||||
@@ -2,15 +2,10 @@ const CLOSEBUTTON = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'
|
||||
|
||||
type MessageType = 'text' | 'html';
|
||||
|
||||
/*
|
||||
These error codes are used to identify the type of error that occurred.
|
||||
The convention is:
|
||||
* PY0 - errors that occur when fetching
|
||||
* PY1 - errors that occur in config
|
||||
* Py2 - errors that occur in plugins
|
||||
* PY9 - Deprecation errors
|
||||
/**
|
||||
* These error codes are used to identify the type of error that occurred.
|
||||
* @see https://docs.pyscript.net/latest/reference/exceptions.html?highlight=errors
|
||||
*/
|
||||
|
||||
export enum ErrorCode {
|
||||
GENERIC = 'PY0000', // Use this only for development then change to a more specific error code
|
||||
FETCH_ERROR = 'PY0001',
|
||||
@@ -29,20 +24,20 @@ export enum ErrorCode {
|
||||
}
|
||||
|
||||
export class UserError extends Error {
|
||||
messageType: MessageType;
|
||||
errorCode: ErrorCode;
|
||||
/**
|
||||
* `isinstance` doesn't work correctly across multiple realms.
|
||||
* Hence, `$$isUserError` flag / marker is used to identify a `UserError`.
|
||||
*/
|
||||
$$isUserError: boolean;
|
||||
|
||||
constructor(errorCode: ErrorCode, message: string, t: MessageType = 'text') {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
constructor(public errorCode: ErrorCode, message: string, public messageType: MessageType = 'text') {
|
||||
super(`(${errorCode}): ${message}`);
|
||||
this.name = 'UserError';
|
||||
this.messageType = t;
|
||||
this.message = `(${errorCode}): ${message}`;
|
||||
this.$$isUserError = true;
|
||||
}
|
||||
}
|
||||
|
||||
export class FetchError extends UserError {
|
||||
errorCode: ErrorCode;
|
||||
constructor(errorCode: ErrorCode, message: string) {
|
||||
super(errorCode, message);
|
||||
this.name = 'FetchError';
|
||||
@@ -50,7 +45,6 @@ export class FetchError extends UserError {
|
||||
}
|
||||
|
||||
export class InstallError extends UserError {
|
||||
errorCode: ErrorCode;
|
||||
constructor(errorCode: ErrorCode, message: string) {
|
||||
super(errorCode, message);
|
||||
this.name = 'InstallError';
|
||||
@@ -63,7 +57,6 @@ export function _createAlertBanner(
|
||||
messageType: MessageType = 'text',
|
||||
logMessage = true,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
switch (`log-${level}-${logMessage}`) {
|
||||
case 'log-error-true':
|
||||
console.error(message);
|
||||
@@ -73,25 +66,21 @@ export function _createAlertBanner(
|
||||
break;
|
||||
}
|
||||
|
||||
const banner = document.createElement('div');
|
||||
banner.className = `alert-banner py-${level}`;
|
||||
|
||||
if (messageType === 'html') {
|
||||
banner.innerHTML = message;
|
||||
} else {
|
||||
banner.textContent = message;
|
||||
}
|
||||
const content = messageType === 'html' ? 'innerHTML' : 'textContent';
|
||||
const banner = Object.assign(document.createElement('div'), {
|
||||
className: `alert-banner py-${level}`,
|
||||
[content]: message,
|
||||
});
|
||||
|
||||
if (level === 'warning') {
|
||||
const closeButton = document.createElement('button');
|
||||
const closeButton = Object.assign(document.createElement('button'), {
|
||||
id: 'alert-close-button',
|
||||
innerHTML: CLOSEBUTTON,
|
||||
});
|
||||
|
||||
closeButton.id = 'alert-close-button';
|
||||
closeButton.addEventListener('click', () => {
|
||||
banner.appendChild(closeButton).addEventListener('click', () => {
|
||||
banner.remove();
|
||||
});
|
||||
closeButton.innerHTML = CLOSEBUTTON;
|
||||
|
||||
banner.appendChild(closeButton);
|
||||
}
|
||||
|
||||
document.body.prepend(banner);
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function robustFetch(url: string, options?: RequestInit): Promise<R
|
||||
`'${error.message}'. Are your filename and path correct?`;
|
||||
} else {
|
||||
errMsg = `PyScript: Access to local files
|
||||
(using "Paths:" in <py-config>)
|
||||
(using [[fetch]] configurations in <py-config>)
|
||||
is not available when directly opening a HTML file;
|
||||
you must use a webserver to serve the additional files.
|
||||
See <a style="text-decoration: underline;" href="https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062">this reference</a>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import { RemoteInterpreter } from './remote_interpreter';
|
||||
import type { PyProxy } from 'pyodide';
|
||||
import type { PyProxyDict, PyProxy } from 'pyodide';
|
||||
import { getLogger } from './logger';
|
||||
import type { Stdio } from './stdio';
|
||||
import * as Synclink from 'synclink';
|
||||
|
||||
const logger = getLogger('pyscript/interpreter');
|
||||
|
||||
@@ -11,37 +12,47 @@ InterpreterClient class is responsible to request code execution
|
||||
(among other things) from a `RemoteInterpreter`
|
||||
*/
|
||||
export class InterpreterClient extends Object {
|
||||
_remote: RemoteInterpreter;
|
||||
_remote: Synclink.Remote<RemoteInterpreter>;
|
||||
config: AppConfig;
|
||||
/**
|
||||
* global symbols table for the underlying interface.
|
||||
* */
|
||||
globals: PyProxy;
|
||||
globals: Synclink.Remote<PyProxyDict>;
|
||||
stdio: Stdio;
|
||||
|
||||
constructor(config: AppConfig, stdio: Stdio) {
|
||||
constructor(config: AppConfig, stdio: Stdio, remote: Synclink.Remote<RemoteInterpreter>) {
|
||||
super();
|
||||
this.config = config;
|
||||
this._remote = new RemoteInterpreter(this.config.interpreters[0].src);
|
||||
this._remote = remote;
|
||||
this.stdio = stdio;
|
||||
}
|
||||
|
||||
/**
|
||||
* initializes the remote interpreter, which further loads the underlying
|
||||
* interface.
|
||||
* */
|
||||
*/
|
||||
async initializeRemote(): Promise<void> {
|
||||
await this._remote.loadInterpreter(this.config, this.stdio);
|
||||
await this._remote.loadInterpreter(this.config, Synclink.proxy(this.stdio));
|
||||
this.globals = this._remote.globals;
|
||||
}
|
||||
|
||||
/**
|
||||
* delegates the code to be run to the underlying interface of
|
||||
* the remote interpreter.
|
||||
* Python exceptions are turned into JS exceptions.
|
||||
* */
|
||||
async run(code: string): Promise<{ result: any }> {
|
||||
return await this._remote.run(code);
|
||||
* Run user Python code. See also the _run_pyscript docstring.
|
||||
*
|
||||
* The result is wrapped in an object to avoid accidentally awaiting a
|
||||
* Python Task or Future returned as the result of the computation.
|
||||
*
|
||||
* @param code the code to run
|
||||
* @param id The id for the default display target (or undefined if no
|
||||
* default display target).
|
||||
* @returns Either:
|
||||
* 1. An Object of the form {result: the_result} if the result is
|
||||
* serializable (or transferable), or
|
||||
* 2. a Synclink Proxy wrapping an object of this if the result is not
|
||||
* serializable.
|
||||
*/
|
||||
async run(code: string, id?: string): Promise<{ result: any }> {
|
||||
return this._remote.pyscript_internal.run_pyscript(code, id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,4 +71,16 @@ export class InterpreterClient extends Object {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async pyimport(mod_name: string): Promise<Synclink.Remote<PyProxy>> {
|
||||
return this._remote.pyimport(mod_name);
|
||||
}
|
||||
|
||||
async mkdir(path: string) {
|
||||
await this._remote.FS.mkdir(path);
|
||||
}
|
||||
|
||||
async writeFile(path: string, content: string) {
|
||||
await this._remote.FS.writeFile(path, content, { encoding: 'utf8' });
|
||||
}
|
||||
}
|
||||
|
||||
26
pyscriptjs/src/interpreter_worker/worker.ts
Normal file
26
pyscriptjs/src/interpreter_worker/worker.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// XXX: what about code duplications?
|
||||
// With the current build configuration, the code for logger,
|
||||
// remote_interpreter and everything which is included from there is
|
||||
// bundled/fetched/executed twice, once in pyscript.js and once in
|
||||
// worker_interpreter.js.
|
||||
|
||||
import { getLogger } from '../logger';
|
||||
import { RemoteInterpreter } from '../remote_interpreter';
|
||||
import * as Synclink from 'synclink';
|
||||
|
||||
const logger = getLogger('worker');
|
||||
logger.info('Interpreter worker starting...');
|
||||
|
||||
async function worker_initialize(cfg) {
|
||||
const remote_interpreter = new RemoteInterpreter(cfg.src);
|
||||
// this is the equivalent of await import(interpreterURL)
|
||||
logger.info(`Downloading ${cfg.name}...`); // XXX we should use logStatus
|
||||
importScripts(cfg.src);
|
||||
|
||||
logger.info('worker_initialize() complete');
|
||||
return Synclink.proxy(remote_interpreter);
|
||||
}
|
||||
|
||||
Synclink.expose(worker_initialize);
|
||||
|
||||
export type { worker_initialize };
|
||||
@@ -27,7 +27,7 @@ interface Logger {
|
||||
debug(message: string, ...args: unknown[]): void;
|
||||
info(message: string, ...args: unknown[]): void;
|
||||
warn(message: string, ...args: unknown[]): void;
|
||||
error(message: string, ...args: unknown[]): void;
|
||||
error(message: string | Error, ...args: unknown[]): void;
|
||||
}
|
||||
|
||||
const _cache = new Map<string, Logger>();
|
||||
@@ -44,8 +44,8 @@ function getLogger(prefix: string): Logger {
|
||||
function _makeLogger(prefix: string): Logger {
|
||||
prefix = `[${prefix}] `;
|
||||
|
||||
function make(level: string) {
|
||||
const out_fn = console[level].bind(console);
|
||||
function make(level: 'info' | 'debug' | 'warn' | 'error') {
|
||||
const out_fn = console[level].bind(console) as typeof console.log;
|
||||
function fn(fmt: string, ...args: unknown[]) {
|
||||
out_fn(prefix + fmt, ...args);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { $$ } from 'basic-devtools';
|
||||
|
||||
import './styles/pyscript_base.css';
|
||||
|
||||
import { loadConfigFromElement } from './pyconfig';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { AppConfig, InterpreterConfig } from './pyconfig';
|
||||
import { InterpreterClient } from './interpreter_client';
|
||||
import { version } from './version';
|
||||
import { PluginManager, define_custom_element } from './plugin';
|
||||
import { PluginManager, Plugin, PythonPlugin } from './plugin';
|
||||
import { make_PyScript, initHandlers, mountElements } from './components/pyscript';
|
||||
import { getLogger } from './logger';
|
||||
import { showWarning, globalExport, createLock } from './utils';
|
||||
import { calculatePaths } from './plugins/fetch';
|
||||
import { showWarning, createLock } from './utils';
|
||||
import { calculateFetchPaths } from './plugins/calculateFetchPaths';
|
||||
import { createCustomElements } from './components/elements';
|
||||
import { UserError, ErrorCode, _createAlertBanner } from './exceptions';
|
||||
import { type Stdio, StdioMultiplexer, DEFAULT_STDIO } from './stdio';
|
||||
@@ -16,13 +17,28 @@ import { PyTerminalPlugin } from './plugins/pyterminal';
|
||||
import { SplashscreenPlugin } from './plugins/splashscreen';
|
||||
import { ImportmapPlugin } from './plugins/importmap';
|
||||
import { StdioDirector as StdioDirector } from './plugins/stdiodirector';
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import pyscript from './python/pyscript/__init__.py';
|
||||
import { RemoteInterpreter } from './remote_interpreter';
|
||||
import { robustFetch } from './fetch';
|
||||
import * as Synclink from 'synclink';
|
||||
|
||||
const logger = getLogger('pyscript/main');
|
||||
|
||||
/**
|
||||
* Monkey patching the error transfer handler to preserve the `$$isUserError`
|
||||
* marker so as to detect `UserError` subclasses in the error handling code.
|
||||
*/
|
||||
const throwHandler = Synclink.transferHandlers.get('throw') as Synclink.TransferHandler<
|
||||
{ value: unknown },
|
||||
{ value: { $$isUserError: boolean } }
|
||||
>;
|
||||
const old_error_transfer_handler = throwHandler.serialize.bind(throwHandler) as typeof throwHandler.serialize;
|
||||
function new_error_transfer_handler({ value }: { value: { $$isUserError: boolean } }) {
|
||||
const result = old_error_transfer_handler({ value });
|
||||
result[0].value.$$isUserError = value.$$isUserError;
|
||||
return result;
|
||||
}
|
||||
throwHandler.serialize = new_error_transfer_handler;
|
||||
|
||||
/* High-level overview of the lifecycle of a PyScript App:
|
||||
|
||||
1. pyscript.js is loaded by the browser. PyScriptApp().main() is called
|
||||
@@ -45,25 +61,25 @@ const logger = getLogger('pyscript/main');
|
||||
user scripts
|
||||
|
||||
8. initialize the rest of web components such as py-button, py-repl, etc.
|
||||
|
||||
More concretely:
|
||||
|
||||
- Points 1-4 are implemented sequentially in PyScriptApp.main().
|
||||
|
||||
- PyScriptApp.loadInterpreter adds a <script> tag to the document to initiate
|
||||
the download, and then adds an event listener for the 'load' event, which
|
||||
in turns calls PyScriptApp.afterInterpreterLoad().
|
||||
|
||||
- PyScriptApp.afterInterpreterLoad() implements all the points >= 5.
|
||||
*/
|
||||
|
||||
export let interpreter;
|
||||
// TODO: This is for backwards compatibility, it should be removed
|
||||
// when we finish the deprecation cycle of `runtime`
|
||||
export let runtime;
|
||||
|
||||
export class PyScriptApp {
|
||||
config: AppConfig;
|
||||
interpreter: InterpreterClient;
|
||||
unwrapped_remote: RemoteInterpreter;
|
||||
readyPromise: Promise<void>;
|
||||
PyScript: ReturnType<typeof make_PyScript>;
|
||||
plugins: PluginManager;
|
||||
_stdioMultiplexer: StdioMultiplexer;
|
||||
tagExecutionLock: ReturnType<typeof createLock>; // this is used to ensure that py-script tags are executed sequentially
|
||||
tagExecutionLock: () => Promise<() => void>; // this is used to ensure that py-script tags are executed sequentially
|
||||
_numPendingTags: number;
|
||||
scriptTagsPromise: Promise<void>;
|
||||
resolvedScriptTags: () => void;
|
||||
|
||||
constructor() {
|
||||
// initialize the builtin plugins
|
||||
@@ -75,6 +91,8 @@ export class PyScriptApp {
|
||||
|
||||
this.plugins.add(new StdioDirector(this._stdioMultiplexer));
|
||||
this.tagExecutionLock = createLock();
|
||||
this._numPendingTags = 0;
|
||||
this.scriptTagsPromise = new Promise(res => (this.resolvedScriptTags = res));
|
||||
}
|
||||
|
||||
// Error handling logic: if during the execution we encounter an error
|
||||
@@ -82,18 +100,33 @@ export class PyScriptApp {
|
||||
// config, file not found in fetch, etc.), we can throw UserError(). It is
|
||||
// responsibility of main() to catch it and show it to the user in a
|
||||
// proper way (e.g. by using a banner at the top of the page).
|
||||
main() {
|
||||
async main() {
|
||||
try {
|
||||
this._realMain();
|
||||
await this._realMain();
|
||||
} catch (error) {
|
||||
this._handleUserErrorMaybe(error);
|
||||
await this._handleUserErrorMaybe(error);
|
||||
}
|
||||
}
|
||||
|
||||
_handleUserErrorMaybe(error) {
|
||||
if (error instanceof UserError) {
|
||||
_createAlertBanner(error.message, 'error', error.messageType);
|
||||
this.plugins.onUserError(error);
|
||||
incrementPendingTags() {
|
||||
this._numPendingTags += 1;
|
||||
}
|
||||
|
||||
decrementPendingTags() {
|
||||
if (this._numPendingTags <= 0) {
|
||||
throw new Error('INTERNAL ERROR: assertion _numPendingTags > 0 failed');
|
||||
}
|
||||
this._numPendingTags -= 1;
|
||||
if (this._numPendingTags === 0) {
|
||||
this.resolvedScriptTags();
|
||||
}
|
||||
}
|
||||
|
||||
async _handleUserErrorMaybe(error: any) {
|
||||
const e = error as UserError;
|
||||
if (e && e.$$isUserError) {
|
||||
_createAlertBanner(e.message, 'error', e.messageType);
|
||||
await this.plugins.onUserError(e);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -102,11 +135,15 @@ export class PyScriptApp {
|
||||
// ============ lifecycle ============
|
||||
|
||||
// lifecycle (1)
|
||||
_realMain() {
|
||||
async _realMain() {
|
||||
this.loadConfig();
|
||||
this.plugins.configure(this.config);
|
||||
await this.plugins.configure(this.config);
|
||||
this.plugins.beforeLaunch(this.config);
|
||||
this.loadInterpreter();
|
||||
await this.loadInterpreter();
|
||||
interpreter = this.unwrapped_remote;
|
||||
// TODO: This is for backwards compatibility, it should be removed
|
||||
// when we finish the deprecation cycle of `runtime`
|
||||
runtime = this.unwrapped_remote;
|
||||
}
|
||||
|
||||
// lifecycle (2)
|
||||
@@ -116,7 +153,7 @@ export class PyScriptApp {
|
||||
// XXX: we should actively complain if there are multiple <py-config>
|
||||
// and show a big error. PRs welcome :)
|
||||
logger.info('searching for <py-config>');
|
||||
const elements = document.getElementsByTagName('py-config');
|
||||
const elements = $$('py-config', document);
|
||||
let el: Element | null = null;
|
||||
if (elements.length > 0) el = elements[0];
|
||||
if (elements.length >= 2) {
|
||||
@@ -126,11 +163,72 @@ export class PyScriptApp {
|
||||
);
|
||||
}
|
||||
this.config = loadConfigFromElement(el);
|
||||
if (this.config.execution_thread === 'worker' && crossOriginIsolated === false) {
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
`When execution_thread is "worker", the site must be cross origin isolated, but crossOriginIsolated is false.
|
||||
To be cross origin isolated, the server must use https and also serve with the following headers: ${JSON.stringify(
|
||||
{
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
},
|
||||
)}.
|
||||
|
||||
The problem may be that one or both of these are missing.
|
||||
`,
|
||||
);
|
||||
}
|
||||
logger.info('config loaded:\n' + JSON.stringify(this.config, null, 2));
|
||||
}
|
||||
|
||||
_get_base_url(): string {
|
||||
// Note that this requires that pyscript is loaded via a <script>
|
||||
// tag. If we want to allow loading via an ES6 module in the future,
|
||||
// we need to think about some other strategy
|
||||
const elem = document.currentScript as HTMLScriptElement;
|
||||
const slash = elem.src.lastIndexOf('/');
|
||||
return elem.src.slice(0, slash);
|
||||
}
|
||||
|
||||
async _startInterpreter_main(interpreter_cfg: InterpreterConfig) {
|
||||
logger.info('Starting the interpreter in the main thread');
|
||||
// this is basically equivalent to worker_initialize()
|
||||
const remote_interpreter = new RemoteInterpreter(interpreter_cfg.src);
|
||||
this.unwrapped_remote = remote_interpreter;
|
||||
const { port1, port2 } = new Synclink.FakeMessageChannel() as unknown as MessageChannel;
|
||||
port1.start();
|
||||
port2.start();
|
||||
Synclink.expose(remote_interpreter, port2);
|
||||
const wrapped_remote_interpreter = Synclink.wrap(port1);
|
||||
|
||||
this.logStatus(`Downloading ${interpreter_cfg.name}...`);
|
||||
/* Dynamically download and import pyodide: the import() puts a
|
||||
loadPyodide() function into globalThis, which is later called by
|
||||
RemoteInterpreter.
|
||||
|
||||
This is suboptimal: ideally, we would like to import() a module
|
||||
which exports loadPyodide(), but this plays badly with workers
|
||||
because at the moment of writing (2023-03-24) Firefox does not
|
||||
support ES modules in workers:
|
||||
https://caniuse.com/mdn-api_worker_worker_ecmascript_modules
|
||||
*/
|
||||
const interpreterURL = interpreter_cfg.src;
|
||||
await import(interpreterURL);
|
||||
return wrapped_remote_interpreter;
|
||||
}
|
||||
|
||||
async _startInterpreter_worker(interpreter_cfg: InterpreterConfig) {
|
||||
logger.warn('execution_thread = "worker" is still VERY experimental, use it at your own risk');
|
||||
logger.info('Starting the interpreter in a web worker');
|
||||
const base_url = this._get_base_url();
|
||||
const worker = new Worker(base_url + '/interpreter_worker.js');
|
||||
const worker_initialize: any = Synclink.wrap(worker);
|
||||
const wrapped_remote_interpreter = await worker_initialize(interpreter_cfg);
|
||||
return wrapped_remote_interpreter;
|
||||
}
|
||||
|
||||
// lifecycle (4)
|
||||
loadInterpreter() {
|
||||
async loadInterpreter() {
|
||||
logger.info('Initializing interpreter');
|
||||
if (this.config.interpreters.length == 0) {
|
||||
throw new UserError(ErrorCode.BAD_CONFIG, 'Fatal error: config.interpreter is empty');
|
||||
@@ -140,26 +238,20 @@ export class PyScriptApp {
|
||||
showWarning('Multiple interpreters are not supported yet.<br />Only the first will be used', 'html');
|
||||
}
|
||||
|
||||
const interpreter_cfg = this.config.interpreters[0];
|
||||
const cfg = this.config.interpreters[0];
|
||||
let wrapped_remote_interpreter;
|
||||
if (this.config.execution_thread == 'worker') {
|
||||
wrapped_remote_interpreter = await this._startInterpreter_worker(cfg);
|
||||
} else {
|
||||
wrapped_remote_interpreter = await this._startInterpreter_main(cfg);
|
||||
}
|
||||
|
||||
this.interpreter = new InterpreterClient(this.config, this._stdioMultiplexer);
|
||||
|
||||
this.logStatus(`Downloading ${interpreter_cfg.name}...`);
|
||||
|
||||
// download pyodide by using a <script> tag. Once it's ready, the
|
||||
// "load" event will be fired and the exeuction logic will continue.
|
||||
// Note that the load event is fired asynchronously and thus any
|
||||
// exception which is throw inside the event handler is *NOT* caught
|
||||
// by the try/catch inside main(): that's why we need to .catch() it
|
||||
// explicitly and call _handleUserErrorMaybe also there.
|
||||
const script = document.createElement('script'); // create a script DOM node
|
||||
script.src = this.interpreter._remote.src;
|
||||
script.addEventListener('load', () => {
|
||||
this.afterInterpreterLoad(this.interpreter).catch(error => {
|
||||
this._handleUserErrorMaybe(error);
|
||||
});
|
||||
});
|
||||
document.head.appendChild(script);
|
||||
this.interpreter = new InterpreterClient(
|
||||
this.config,
|
||||
this._stdioMultiplexer,
|
||||
wrapped_remote_interpreter as Synclink.Remote<RemoteInterpreter>,
|
||||
);
|
||||
await this.afterInterpreterLoad(this.interpreter);
|
||||
}
|
||||
|
||||
// lifecycle (5)
|
||||
@@ -179,24 +271,25 @@ export class PyScriptApp {
|
||||
await mountElements(interpreter);
|
||||
|
||||
// lifecycle (6.5)
|
||||
this.plugins.afterSetup(interpreter);
|
||||
await this.plugins.afterSetup(interpreter);
|
||||
|
||||
//Refresh module cache in case plugins have modified the filesystem
|
||||
interpreter._remote.invalidate_module_path_cache();
|
||||
await interpreter._remote.invalidate_module_path_cache();
|
||||
this.logStatus('Executing <py-script> tags...');
|
||||
this.executeScripts(interpreter);
|
||||
await this.executeScripts(interpreter);
|
||||
|
||||
this.logStatus('Initializing web components...');
|
||||
// lifecycle (8)
|
||||
createCustomElements(interpreter);
|
||||
|
||||
//Takes a runtime and a reference to the PyScriptApp (to access plugins)
|
||||
createCustomElements(interpreter, this);
|
||||
initHandlers(interpreter);
|
||||
|
||||
// NOTE: interpreter message is used by integration tests to know that
|
||||
// pyscript initialization has complete. If you change it, you need to
|
||||
// change it also in tests/integration/support.py
|
||||
this.logStatus('Startup complete');
|
||||
this.plugins.afterStartup(interpreter);
|
||||
await this.plugins.afterStartup(interpreter);
|
||||
logger.info('PyScript page fully initialized');
|
||||
}
|
||||
|
||||
@@ -205,58 +298,33 @@ export class PyScriptApp {
|
||||
// XXX: maybe the following calls could be parallelized, instead of
|
||||
// await()ing immediately. For now I'm using await to be 100%
|
||||
// compatible with the old behavior.
|
||||
logger.info('importing pyscript');
|
||||
|
||||
// Save and load pyscript.py from FS
|
||||
interpreter._remote.interface.FS.mkdirTree('/home/pyodide/pyscript');
|
||||
interpreter._remote.interface.FS.writeFile('pyscript/__init__.py', pyscript);
|
||||
//Refresh the module cache so Python consistently finds pyscript module
|
||||
interpreter._remote.invalidate_module_path_cache();
|
||||
|
||||
// inject `define_custom_element` and showWarning it into the PyScript
|
||||
// module scope
|
||||
const pyscript_module = interpreter._remote.interface.pyimport('pyscript');
|
||||
pyscript_module.define_custom_element = define_custom_element;
|
||||
pyscript_module.showWarning = showWarning;
|
||||
pyscript_module._set_version_info(version);
|
||||
pyscript_module.destroy();
|
||||
|
||||
// import some carefully selected names into the global namespace
|
||||
await interpreter.run(`
|
||||
import js
|
||||
import pyscript
|
||||
from pyscript import Element, display, HTML
|
||||
pyscript._install_deprecated_globals_2022_12_1(globals())
|
||||
`);
|
||||
|
||||
if (this.config.packages) {
|
||||
logger.info('Packages to install: ', this.config.packages);
|
||||
await interpreter._remote.installPackage(this.config.packages);
|
||||
}
|
||||
await this.fetchPaths(interpreter);
|
||||
await Promise.all([this.installPackages(), this.fetchPaths(interpreter)]);
|
||||
|
||||
//This may be unnecessary - only useful if plugins try to import files fetch'd in fetchPaths()
|
||||
interpreter._remote.invalidate_module_path_cache();
|
||||
await interpreter._remote.invalidate_module_path_cache();
|
||||
// Finally load plugins
|
||||
await this.fetchUserPlugins(interpreter);
|
||||
}
|
||||
|
||||
async fetchPaths(interpreter: InterpreterClient) {
|
||||
// XXX this can be VASTLY improved: for each path we need to fetch a
|
||||
// URL and write to the virtual filesystem: pyodide.loadFromFile does
|
||||
// it in Python, which means we need to have the interpreter
|
||||
// initialized. But we could easily do it in JS in parallel with the
|
||||
// download/startup of pyodide.
|
||||
const [paths, fetchPaths] = calculatePaths(this.config.fetch);
|
||||
logger.info('Paths to write: ', paths);
|
||||
logger.info('Paths to fetch: ', fetchPaths);
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
logger.info(` fetching path: ${fetchPaths[i]}`);
|
||||
|
||||
// Exceptions raised from here will create an alert banner
|
||||
await interpreter._remote.loadFromFile(paths[i], fetchPaths[i]);
|
||||
async installPackages() {
|
||||
if (!this.config.packages) {
|
||||
return;
|
||||
}
|
||||
logger.info('All paths fetched');
|
||||
logger.info('Packages to install: ', this.config.packages);
|
||||
await this.interpreter._remote.installPackage(this.config.packages);
|
||||
}
|
||||
|
||||
async fetchPaths(interpreter: InterpreterClient) {
|
||||
// TODO: start fetching before interpreter initialization
|
||||
const paths = calculateFetchPaths(this.config.fetch);
|
||||
logger.info('Fetching urls:', paths.map(({ url }) => url).join(', '));
|
||||
await Promise.all(
|
||||
paths.map(async ({ path, url }) => {
|
||||
await interpreter._remote.loadFileFromURL(path, url);
|
||||
logger.info(` Fetched ${url} ==> ${path}`);
|
||||
}),
|
||||
);
|
||||
logger.info('Fetched all paths');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,7 +371,7 @@ export class PyScriptApp {
|
||||
const blobFile = new File([pluginBlob], 'plugin.js', { type: 'text/javascript' });
|
||||
const fileUrl = URL.createObjectURL(blobFile);
|
||||
|
||||
const module = await import(fileUrl);
|
||||
const module = (await import(fileUrl)) as { default: { new (): Plugin } };
|
||||
// Note: We have to put module.default in a variable
|
||||
// because we have seen weird behaviour when doing
|
||||
// new module.default() directly.
|
||||
@@ -333,10 +401,10 @@ export class PyScriptApp {
|
||||
const pathArr = filePath.split('/');
|
||||
const filename = pathArr.pop();
|
||||
// TODO: Would be probably be better to store plugins somewhere like /plugins/python/ or similar
|
||||
await interpreter._remote.loadFromFile(filename, filePath);
|
||||
await interpreter._remote.loadFileFromURL(filename, filePath);
|
||||
|
||||
//refresh module cache before trying to import module files into interpreter
|
||||
interpreter._remote.invalidate_module_path_cache();
|
||||
await interpreter._remote.invalidate_module_path_cache();
|
||||
|
||||
const modulename = filePath.replace(/^.*[\\/]/, '').replace('.py', '');
|
||||
|
||||
@@ -344,9 +412,9 @@ export class PyScriptApp {
|
||||
// TODO: This is very specific to Pyodide API and will not work for other interpreters,
|
||||
// when we add support for other interpreters we will need to move this to the
|
||||
// interpreter API level and allow each one to implement it in its own way
|
||||
const module = interpreter._remote.interface.pyimport(modulename);
|
||||
if (typeof module.plugin !== 'undefined') {
|
||||
const py_plugin = module.plugin;
|
||||
const module = await interpreter.pyimport(modulename);
|
||||
if (typeof (await module.plugin) !== 'undefined') {
|
||||
const py_plugin = (await module.plugin) as PythonPlugin;
|
||||
py_plugin.init(this);
|
||||
this.plugins.addPythonPlugin(py_plugin);
|
||||
} else {
|
||||
@@ -356,10 +424,14 @@ modules must contain a "plugin" attribute. For more information check the plugin
|
||||
}
|
||||
|
||||
// lifecycle (7)
|
||||
executeScripts(interpreter: InterpreterClient) {
|
||||
async executeScripts(interpreter: InterpreterClient) {
|
||||
// make_PyScript takes an interpreter and a PyScriptApp as arguments
|
||||
this.PyScript = make_PyScript(interpreter, this);
|
||||
customElements.define('py-script', this.PyScript);
|
||||
this.incrementPendingTags();
|
||||
this.decrementPendingTags();
|
||||
await this.scriptTagsPromise;
|
||||
await this.interpreter._remote.pyscript_internal.schedule_deferred_tasks();
|
||||
}
|
||||
|
||||
// ================= registraton API ====================
|
||||
@@ -375,17 +447,14 @@ modules must contain a "plugin" attribute. For more information check the plugin
|
||||
}
|
||||
}
|
||||
|
||||
function pyscript_get_config() {
|
||||
return globalApp.config;
|
||||
}
|
||||
globalExport('pyscript_get_config', pyscript_get_config);
|
||||
globalThis.pyscript_get_config = () => globalApp.config;
|
||||
|
||||
// main entry point of execution
|
||||
const globalApp = new PyScriptApp();
|
||||
globalApp.main();
|
||||
|
||||
export { version };
|
||||
export const interpreter = globalApp.interpreter;
|
||||
// TODO: This is for backwards compatibility, it should be removed
|
||||
// when we finish the deprecation cycle of `runtime`
|
||||
export const runtime = globalApp.interpreter;
|
||||
// This top level execution causes trouble in jest
|
||||
if (typeof jest === 'undefined') {
|
||||
globalApp.readyPromise = globalApp.main();
|
||||
}
|
||||
|
||||
export { version } from './version';
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { PyScriptApp } from './main';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { UserError } from './exceptions';
|
||||
import { UserError, ErrorCode } from './exceptions';
|
||||
import { getLogger } from './logger';
|
||||
import { make_PyScript } from './components/pyscript';
|
||||
import { InterpreterClient } from './interpreter_client';
|
||||
import { make_PyRepl } from './components/pyrepl';
|
||||
|
||||
const logger = getLogger('plugin');
|
||||
type PyScriptTag = InstanceType<ReturnType<typeof make_PyScript>>;
|
||||
type PyReplTag = InstanceType<ReturnType<typeof make_PyRepl>>;
|
||||
|
||||
export class Plugin {
|
||||
/** Validate the configuration of the plugin and handle default values.
|
||||
@@ -20,7 +23,9 @@ export class Plugin {
|
||||
* This hook should **NOT** contain expensive operations, else it delays
|
||||
* the download of the python interpreter which is initiated later.
|
||||
*/
|
||||
configure(config: AppConfig) {}
|
||||
configure(_config: AppConfig) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
/** The preliminary initialization phase is complete and we are about to
|
||||
* download and launch the Python interpreter.
|
||||
@@ -32,53 +37,128 @@ export class Plugin {
|
||||
* This hook should **NOT** contain expensive operations, else it delays
|
||||
* the download of the python interpreter which is initiated later.
|
||||
*/
|
||||
beforeLaunch(config: AppConfig) {}
|
||||
beforeLaunch(_config: AppConfig) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
/** The Python interpreter has been launched, the virtualenv has been
|
||||
* installed and we are ready to execute user code.
|
||||
*
|
||||
* The <py-script> tags will be executed after this hook.
|
||||
*/
|
||||
afterSetup(interpreter: InterpreterClient) {}
|
||||
afterSetup(_interpreter: InterpreterClient) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
/** The source of a <py-script>> tag has been fetched, and we're about
|
||||
* to evaluate that source using the provided interpreter.
|
||||
*
|
||||
* @param options.interpreter The Interpreter object that will be used to evaluated the Python source code
|
||||
* @param options.interpreter The Interpreter object that will be used to evaluate the Python source code
|
||||
* @param options.src {string} The Python source code to be evaluated
|
||||
* @param options.pyScriptTag The <py-script> HTML tag that originated the evaluation
|
||||
*/
|
||||
beforePyScriptExec(options: { interpreter: InterpreterClient; src: string; pyScriptTag: PyScriptTag }) {}
|
||||
beforePyScriptExec(_options: { interpreter: InterpreterClient; src: string; pyScriptTag: PyScriptTag }) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
/** The Python in a <py-script> has just been evaluated, but control
|
||||
* has not been ceded back to the JavaScript event loop yet
|
||||
*
|
||||
* @param options.interpreter The Interpreter object that will be used to evaluated the Python source code
|
||||
* @param options.interpreter The Interpreter object that will be used to evaluate the Python source code
|
||||
* @param options.src {string} The Python source code to be evaluated
|
||||
* @param options.pyScriptTag The <py-script> HTML tag that originated the evaluation
|
||||
* @param options.result The returned result of evaluating the Python (if any)
|
||||
*/
|
||||
afterPyScriptExec(options: {
|
||||
afterPyScriptExec(_options: {
|
||||
interpreter: InterpreterClient;
|
||||
src: string;
|
||||
pyScriptTag: PyScriptTag;
|
||||
result: any;
|
||||
}) {}
|
||||
}) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
/** The source of the <py-repl> tag has been fetched and its output-element determined;
|
||||
* we're about to evaluate the source using the provided interpreter
|
||||
*
|
||||
* @param options.interpreter The interpreter object that will be used to evaluated the Python source code
|
||||
* @param options.src {string} The Python source code to be evaluated
|
||||
* @param options.outEl The element that the result of the REPL evaluation will be output to.
|
||||
* @param options.pyReplTag The <py-repl> HTML tag the originated the evaluation
|
||||
*/
|
||||
beforePyReplExec(options: {
|
||||
interpreter: InterpreterClient;
|
||||
src: string;
|
||||
outEl: HTMLElement;
|
||||
pyReplTag: PyReplTag;
|
||||
}) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options.interpreter The interpreter object that will be used to evaluated the Python source code
|
||||
* @param options.src {string} The Python source code to be evaluated
|
||||
* @param options.outEl The element that the result of the REPL evaluation will be output to.
|
||||
* @param options.pyReplTag The <py-repl> HTML tag the originated the evaluation
|
||||
* @param options.result The result of evaluating the Python (if any)
|
||||
*/
|
||||
afterPyReplExec(options: {
|
||||
interpreter: InterpreterClient;
|
||||
src: string;
|
||||
outEl: HTMLElement;
|
||||
pyReplTag: PyReplTag;
|
||||
result: any;
|
||||
}) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
/** Startup complete. The interpreter is initialized and ready, user
|
||||
* scripts have been executed: the main initialization logic ends here and
|
||||
* the page is ready to accept user interactions.
|
||||
*/
|
||||
afterStartup(interpreter: InterpreterClient) {}
|
||||
afterStartup(_interpreter: InterpreterClient) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
/** Called when an UserError is raised
|
||||
*/
|
||||
onUserError(error: UserError) {}
|
||||
onUserError(_error: UserError) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
export type PythonPlugin = {
|
||||
init(app: PyScriptApp): void;
|
||||
configure?: (config: AppConfig) => Promise<void>;
|
||||
afterSetup?: (interpreter: InterpreterClient) => Promise<void>;
|
||||
afterStartup?: (interpreter: InterpreterClient) => Promise<void>;
|
||||
beforePyScriptExec?: (interpreter: InterpreterClient, src: string, pyScriptTag: PyScriptTag) => Promise<void>;
|
||||
afterPyScriptExec?: (
|
||||
interpreter: InterpreterClient,
|
||||
src: string,
|
||||
pyScriptTag: PyScriptTag,
|
||||
result: any,
|
||||
) => Promise<void>;
|
||||
beforePyReplExec?: (
|
||||
interpreter: InterpreterClient,
|
||||
src: string,
|
||||
outEl: HTMLElement,
|
||||
pyReplTag: PyReplTag,
|
||||
) => Promise<void>;
|
||||
afterPyReplExec?: (
|
||||
interpreter: InterpreterClient,
|
||||
src: string,
|
||||
outEl: HTMLElement,
|
||||
pyReplTag: PyReplTag,
|
||||
result: any,
|
||||
) => Promise<void>;
|
||||
onUserError?: (error: UserError) => Promise<void>;
|
||||
};
|
||||
|
||||
export class PluginManager {
|
||||
_plugins: Plugin[];
|
||||
_pythonPlugins: any[];
|
||||
_pythonPlugins: PythonPlugin[];
|
||||
|
||||
constructor() {
|
||||
this._plugins = [];
|
||||
@@ -86,17 +166,17 @@ export class PluginManager {
|
||||
}
|
||||
|
||||
add(...plugins: Plugin[]) {
|
||||
for (const p of plugins) this._plugins.push(p);
|
||||
this._plugins.push(...plugins);
|
||||
}
|
||||
|
||||
addPythonPlugin(plugin: any) {
|
||||
addPythonPlugin(plugin: PythonPlugin) {
|
||||
this._pythonPlugins.push(plugin);
|
||||
}
|
||||
|
||||
configure(config: AppConfig) {
|
||||
for (const p of this._plugins) p.configure?.(config);
|
||||
|
||||
for (const p of this._pythonPlugins) p.configure?.(config);
|
||||
async configure(config: AppConfig) {
|
||||
const fn = p => p.configure?.(config);
|
||||
await Promise.all(this._plugins.map(fn));
|
||||
await Promise.all(this._pythonPlugins.map(fn));
|
||||
}
|
||||
|
||||
beforeLaunch(config: AppConfig) {
|
||||
@@ -109,43 +189,93 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
afterSetup(interpreter: InterpreterClient) {
|
||||
async afterSetup(interpreter: InterpreterClient) {
|
||||
const promises = [];
|
||||
for (const p of this._plugins) {
|
||||
try {
|
||||
p.afterSetup?.(interpreter);
|
||||
promises.push(p.afterSetup?.(interpreter));
|
||||
} catch (e) {
|
||||
logger.error(`Error while calling afterSetup hook of plugin ${p.constructor.name}`, e);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
for (const p of this._pythonPlugins) p.afterSetup?.(interpreter);
|
||||
for (const p of this._pythonPlugins) await p.afterSetup?.(interpreter);
|
||||
}
|
||||
|
||||
afterStartup(interpreter: InterpreterClient) {
|
||||
for (const p of this._plugins) p.afterStartup?.(interpreter);
|
||||
|
||||
for (const p of this._pythonPlugins) p.afterStartup?.(interpreter);
|
||||
async afterStartup(interpreter: InterpreterClient) {
|
||||
const fn = p => p.afterStartup?.(interpreter);
|
||||
await Promise.all(this._plugins.map(fn));
|
||||
await Promise.all(this._pythonPlugins.map(fn));
|
||||
}
|
||||
|
||||
beforePyScriptExec(options: { interpreter: InterpreterClient; src: string; pyScriptTag: PyScriptTag }) {
|
||||
for (const p of this._plugins) p.beforePyScriptExec?.(options);
|
||||
|
||||
for (const p of this._pythonPlugins) p.beforePyScriptExec?.callKwargs(options);
|
||||
async beforePyScriptExec(options: { interpreter: InterpreterClient; src: string; pyScriptTag: PyScriptTag }) {
|
||||
await Promise.all(this._plugins.map(p => p.beforePyScriptExec?.(options)));
|
||||
await Promise.all(
|
||||
this._pythonPlugins.map(p => p.beforePyScriptExec?.(options.interpreter, options.src, options.pyScriptTag)),
|
||||
);
|
||||
}
|
||||
|
||||
afterPyScriptExec(options: { interpreter: InterpreterClient; src: string; pyScriptTag: PyScriptTag; result: any }) {
|
||||
for (const p of this._plugins) p.afterPyScriptExec?.(options);
|
||||
|
||||
for (const p of this._pythonPlugins) p.afterPyScriptExec?.callKwargs(options);
|
||||
async afterPyScriptExec(options: {
|
||||
interpreter: InterpreterClient;
|
||||
src: string;
|
||||
pyScriptTag: PyScriptTag;
|
||||
result: any;
|
||||
}) {
|
||||
await Promise.all(this._plugins.map(p => p.afterPyScriptExec?.(options)));
|
||||
await Promise.all(
|
||||
this._pythonPlugins.map(
|
||||
p => p.afterPyScriptExec?.(options.interpreter, options.src, options.pyScriptTag, options.result),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
onUserError(error: UserError) {
|
||||
for (const p of this._plugins) p.onUserError?.(error);
|
||||
async beforePyReplExec(options: {
|
||||
interpreter: InterpreterClient;
|
||||
src: string;
|
||||
outEl: HTMLElement;
|
||||
pyReplTag: PyReplTag;
|
||||
}) {
|
||||
await Promise.all(this._plugins.map(p => p.beforePyReplExec?.(options)));
|
||||
await Promise.all(
|
||||
this._pythonPlugins.map(
|
||||
p => p.beforePyReplExec?.(options.interpreter, options.src, options.outEl, options.pyReplTag),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (const p of this._pythonPlugins) p.onUserError?.(error);
|
||||
async afterPyReplExec(options: {
|
||||
interpreter: InterpreterClient;
|
||||
src: string;
|
||||
outEl: HTMLElement;
|
||||
pyReplTag: PyReplTag;
|
||||
result: any;
|
||||
}) {
|
||||
await Promise.all(this._plugins.map(p => p.afterPyReplExec?.(options)));
|
||||
await Promise.all(
|
||||
this._pythonPlugins.map(
|
||||
p =>
|
||||
p.afterPyReplExec?.(
|
||||
options.interpreter,
|
||||
options.src,
|
||||
options.outEl,
|
||||
options.pyReplTag,
|
||||
options.result,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async onUserError(error: UserError) {
|
||||
const fn = p => p.onUserError?.(error);
|
||||
await Promise.all(this._plugins.map(fn));
|
||||
await Promise.all(this._pythonPlugins.map(fn));
|
||||
}
|
||||
}
|
||||
|
||||
type PyElementInstance = { connect(): void };
|
||||
type PyElementClass = (htmlElement: HTMLElement) => PyElementInstance;
|
||||
|
||||
/**
|
||||
* Defines a new CustomElement (via customElement.defines) with `tag`,
|
||||
* where the new CustomElement is a proxy that delegates the logic to
|
||||
@@ -157,30 +287,99 @@ export class PluginManager {
|
||||
* received by the newly created CustomElement will be
|
||||
* delegated to that instance.
|
||||
*/
|
||||
export function define_custom_element(tag: string, pyPluginClass: any): any {
|
||||
export function define_custom_element(tag: string, pyElementClass: PyElementClass): any {
|
||||
logger.info(`creating plugin: ${tag}`);
|
||||
class ProxyCustomElement extends HTMLElement {
|
||||
shadow: ShadowRoot;
|
||||
wrapper: HTMLElement;
|
||||
pyPluginInstance: any;
|
||||
pyElementInstance: PyElementInstance;
|
||||
originalInnerHTML: string;
|
||||
|
||||
constructor() {
|
||||
logger.debug(`creating ${tag} plugin instance`);
|
||||
super();
|
||||
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
this.wrapper = document.createElement('slot');
|
||||
this.shadow.appendChild(this.wrapper);
|
||||
this.attachShadow({ mode: 'open' }).appendChild(this.wrapper);
|
||||
this.originalInnerHTML = this.innerHTML;
|
||||
this.pyPluginInstance = pyPluginClass(this);
|
||||
this.pyElementInstance = pyElementClass(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const innerHTML = this.pyPluginInstance.connect();
|
||||
const innerHTML = this.pyElementInstance.connect();
|
||||
if (typeof innerHTML === 'string') this.innerHTML = innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(tag, ProxyCustomElement);
|
||||
}
|
||||
|
||||
// Members of py-config in plug that we want to validate must be one of these types
|
||||
type BaseConfigObject = string | boolean | number | undefined;
|
||||
|
||||
/**
|
||||
* Validate that parameter the user provided to py-config conforms to the specified validation function;
|
||||
* if not, throw an error explaining the bad value. If no value is provided, set the parameter
|
||||
* to the provided default value
|
||||
* This is the most generic validation function; other validation functions for common situations follow
|
||||
* @param options.config - The (extended) AppConfig object from py-config
|
||||
* @param {string} options.name - The name of the key in py-config to be checked
|
||||
* @param {(b:BaseConfigObject) => boolean} options.validator - the validation function used to test the user-supplied value
|
||||
* @param {BaseConfigObject} options.defaultValue - The default value for this parameter, if none is provided
|
||||
* @param {string} [options.hintMessage] - The message to show in a user error if the supplied value isn't valid
|
||||
*/
|
||||
export function validateConfigParameter(options: {
|
||||
config: AppConfig;
|
||||
name: string;
|
||||
validator: (b: BaseConfigObject) => boolean;
|
||||
defaultValue: BaseConfigObject;
|
||||
hintMessage?: string;
|
||||
}) {
|
||||
//Validate that the default value is acceptable, at runtime
|
||||
if (!options.validator(options.defaultValue)) {
|
||||
throw Error(
|
||||
`Default value ${JSON.stringify(options.defaultValue)} for ${options.name} is not a valid argument, ` +
|
||||
`according to the provided validator function. ${options.hintMessage ? options.hintMessage : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
const value = options.config[options.name] as BaseConfigObject;
|
||||
if (value !== undefined && !options.validator(value)) {
|
||||
//Use default hint message if none is provided:
|
||||
const hintOutput = `Invalid value ${JSON.stringify(value)} for config.${options.name}. ${
|
||||
options.hintMessage ? options.hintMessage : ''
|
||||
}`;
|
||||
throw new UserError(ErrorCode.BAD_CONFIG, hintOutput);
|
||||
}
|
||||
if (value === undefined) {
|
||||
options.config[options.name] = options.defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that parameter the user provided to py-config is one of the acceptable values in
|
||||
* the given Array; if not, throw an error explaining the bad value. If no value is provided,
|
||||
* set the parameter to the provided default value
|
||||
* @param options.config - The (extended) AppConfig object from py-config
|
||||
* @param {string} options.name - The name of the key in py-config to be checked
|
||||
* @param {Array<BaseConfigObject>} options.possibleValues: The acceptable values for this parameter
|
||||
* @param {BaseConfigObject} options.defaultValue: The default value for this parameter, if none is provided
|
||||
*/
|
||||
export function validateConfigParameterFromArray(options: {
|
||||
config: AppConfig;
|
||||
name: string;
|
||||
possibleValues: Array<BaseConfigObject>;
|
||||
defaultValue: BaseConfigObject;
|
||||
}) {
|
||||
const validator = (b: BaseConfigObject) => options.possibleValues.includes(b);
|
||||
const hint = `The only accepted values are: [${options.possibleValues
|
||||
.map(item => JSON.stringify(item))
|
||||
.join(', ')}]`;
|
||||
|
||||
validateConfigParameter({
|
||||
config: options.config,
|
||||
name: options.name,
|
||||
validator: validator,
|
||||
defaultValue: options.defaultValue,
|
||||
hintMessage: hint,
|
||||
});
|
||||
}
|
||||
|
||||
26
pyscriptjs/src/plugins/calculateFetchPaths.ts
Normal file
26
pyscriptjs/src/plugins/calculateFetchPaths.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { joinPaths } from '../utils';
|
||||
import { FetchConfig } from '../pyconfig';
|
||||
import { UserError, ErrorCode } from '../exceptions';
|
||||
|
||||
export function calculateFetchPaths(fetch_cfg: FetchConfig[]): { url: string; path: string }[] {
|
||||
for (const { files, to_file, from = '' } of fetch_cfg) {
|
||||
if (files !== undefined && to_file !== undefined) {
|
||||
throw new UserError(ErrorCode.BAD_CONFIG, `Cannot use 'to_file' and 'files' parameters together!`);
|
||||
}
|
||||
if (files === undefined && to_file === undefined && from.endsWith('/')) {
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
`Couldn't determine the filename from the path ${from}, please supply 'to_file' parameter.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return fetch_cfg.flatMap(function ({ from = '', to_folder = '.', to_file, files }) {
|
||||
if (files !== undefined) {
|
||||
return files.map(file => ({ url: joinPaths([from, file]), path: joinPaths([to_folder, file]) }));
|
||||
}
|
||||
const filename = to_file || from.slice(1 + from.lastIndexOf('/'));
|
||||
const to_path = joinPaths([to_folder, filename]);
|
||||
return [{ url: from, path: to_path }];
|
||||
});
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { joinPaths } from '../utils';
|
||||
import { FetchConfig } from '../pyconfig';
|
||||
import { UserError, ErrorCode } from '../exceptions';
|
||||
|
||||
export function calculatePaths(fetch_cfg: FetchConfig[]) {
|
||||
const fetchPaths: string[] = [];
|
||||
const paths: string[] = [];
|
||||
fetch_cfg.forEach(function (each_fetch_cfg: FetchConfig) {
|
||||
const from = each_fetch_cfg.from || '';
|
||||
const to_folder = each_fetch_cfg.to_folder || '.';
|
||||
const to_file = each_fetch_cfg.to_file;
|
||||
const files = each_fetch_cfg.files;
|
||||
if (files !== undefined) {
|
||||
if (to_file !== undefined) {
|
||||
throw new UserError(ErrorCode.BAD_CONFIG, `Cannot use 'to_file' and 'files' parameters together!`);
|
||||
}
|
||||
for (const each_f of files) {
|
||||
const each_fetch_path = joinPaths([from, each_f]);
|
||||
fetchPaths.push(each_fetch_path);
|
||||
const each_path = joinPaths([to_folder, each_f]);
|
||||
paths.push(each_path);
|
||||
}
|
||||
} else {
|
||||
fetchPaths.push(from);
|
||||
const filename = to_file || from.split('/').pop();
|
||||
if (filename === '') {
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
`Couldn't determine the filename from the path ${from}, please supply 'to_file' parameter.`,
|
||||
);
|
||||
} else {
|
||||
paths.push(joinPaths([to_folder, filename]));
|
||||
}
|
||||
}
|
||||
});
|
||||
return [paths, fetchPaths];
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { $$ } from 'basic-devtools';
|
||||
|
||||
import { showWarning } from '../utils';
|
||||
import { Plugin } from '../plugin';
|
||||
import { getLogger } from '../logger';
|
||||
@@ -21,11 +23,12 @@ export class ImportmapPlugin extends Plugin {
|
||||
// await the module to be fully registered before executing the code
|
||||
// inside py-script. It's also unclear whether we want to wait or not
|
||||
// (or maybe only wait only if we do an actual 'import'?)
|
||||
for (const node of document.querySelectorAll("script[type='importmap']")) {
|
||||
for (const node of $$("script[type='importmap']", document)) {
|
||||
const importmap: ImportMapType = (() => {
|
||||
try {
|
||||
return JSON.parse(node.textContent) as ImportMapType;
|
||||
} catch (error) {
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
showWarning('Failed to parse import map: ' + error.message);
|
||||
}
|
||||
})();
|
||||
@@ -46,7 +49,7 @@ export class ImportmapPlugin extends Plugin {
|
||||
}
|
||||
|
||||
logger.info('Registering JS module', name);
|
||||
interpreter._remote.registerJsModule(name, exports);
|
||||
await interpreter._remote.registerJsModule(name, exports);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { $ } from 'basic-devtools';
|
||||
|
||||
import type { PyScriptApp } from '../main';
|
||||
import type { AppConfig } from '../pyconfig';
|
||||
import { Plugin } from '../plugin';
|
||||
import { UserError, ErrorCode } from '../exceptions';
|
||||
import { Plugin, validateConfigParameterFromArray } from '../plugin';
|
||||
import { getLogger } from '../logger';
|
||||
import { type Stdio } from '../stdio';
|
||||
import { InterpreterClient } from '../interpreter_client';
|
||||
import { Terminal as TerminalType } from 'xterm';
|
||||
|
||||
const knownPyTerminalTags: WeakSet<HTMLElement> = new WeakSet();
|
||||
|
||||
type AppConfigStyle = AppConfig & {
|
||||
terminal?: boolean | 'auto';
|
||||
docked?: boolean | 'docked';
|
||||
xterm?: boolean | 'xterm';
|
||||
};
|
||||
|
||||
const logger = getLogger('py-terminal');
|
||||
|
||||
@@ -16,41 +26,51 @@ export class PyTerminalPlugin extends Plugin {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
configure(config: AppConfig) {
|
||||
configure(config: AppConfigStyle) {
|
||||
// validate the terminal config and handle default values
|
||||
const t = config.terminal;
|
||||
if (t !== undefined && t !== true && t !== false && t !== 'auto') {
|
||||
const got = JSON.stringify(t);
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
'Invalid value for config.terminal: the only accepted' +
|
||||
`values are true, false and "auto", got "${got}".`,
|
||||
);
|
||||
}
|
||||
if (t === undefined) {
|
||||
config.terminal = 'auto'; // default value
|
||||
}
|
||||
validateConfigParameterFromArray({
|
||||
config: config,
|
||||
name: 'terminal',
|
||||
possibleValues: [true, false, 'auto'],
|
||||
defaultValue: 'auto',
|
||||
});
|
||||
validateConfigParameterFromArray({
|
||||
config: config,
|
||||
name: 'docked',
|
||||
possibleValues: [true, false, 'docked'],
|
||||
defaultValue: 'docked',
|
||||
});
|
||||
validateConfigParameterFromArray({
|
||||
config: config,
|
||||
name: 'xterm',
|
||||
possibleValues: [true, false, 'xterm'],
|
||||
defaultValue: false,
|
||||
});
|
||||
}
|
||||
|
||||
beforeLaunch(config: AppConfig) {
|
||||
beforeLaunch(config: AppConfigStyle) {
|
||||
// if config.terminal is "yes" or "auto", let's add a <py-terminal> to
|
||||
// the document, unless it's already present.
|
||||
const t = config.terminal;
|
||||
if (t === true || t === 'auto') {
|
||||
if (document.querySelector('py-terminal') === null) {
|
||||
const { terminal: t, docked: d, xterm: x } = config;
|
||||
const auto = t === true || t === 'auto';
|
||||
const docked = d === true || d === 'docked';
|
||||
const xterm = x === true || x === 'xterm';
|
||||
if (auto && $('py-terminal', document) === null) {
|
||||
logger.info('No <py-terminal> found, adding one');
|
||||
const termElem = document.createElement('py-terminal');
|
||||
if (t === 'auto') termElem.setAttribute('auto', '');
|
||||
if (auto) termElem.setAttribute('auto', '');
|
||||
if (docked) termElem.setAttribute('docked', '');
|
||||
if (xterm) termElem.setAttribute('xterm', '');
|
||||
document.body.appendChild(termElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterSetup(interpreter: InterpreterClient) {
|
||||
afterSetup(_interpreter: InterpreterClient) {
|
||||
// the Python interpreter has been initialized and we are ready to
|
||||
// execute user code:
|
||||
//
|
||||
// 1. define the "py-terminal" custom element
|
||||
// 1. define the "py-terminal" custom element, either a <pre> element
|
||||
// or using xterm.js
|
||||
//
|
||||
// 2. if there is a <py-terminal> tag on the page, it will register
|
||||
// a Stdio listener just before the user code executes, ensuring
|
||||
@@ -59,33 +79,27 @@ export class PyTerminalPlugin extends Plugin {
|
||||
// 3. everything which was written to stdout BEFORE this moment will
|
||||
// NOT be shown on the py-terminal; in particular, pyodide
|
||||
// startup messages will not be shown (but they will go to the
|
||||
// console as usual). This is by design, else we would display
|
||||
// e.g. "Python initialization complete" on every page, which we
|
||||
// don't want.
|
||||
// console as usual).
|
||||
//
|
||||
// 4. (in the future we might want to add an option to start the
|
||||
// capture earlier, but I don't think it's important now).
|
||||
const PyTerminal = make_PyTerminal(this.app);
|
||||
const PyTerminal = _interpreter.config.xterm ? make_PyTerminal_xterm(this.app) : make_PyTerminal_pre(this.app);
|
||||
customElements.define('py-terminal', PyTerminal);
|
||||
}
|
||||
}
|
||||
|
||||
function make_PyTerminal(app: PyScriptApp) {
|
||||
/** The <py-terminal> custom element, which automatically register a stdio
|
||||
* listener to capture and display stdout/stderr
|
||||
*/
|
||||
class PyTerminal extends HTMLElement implements Stdio {
|
||||
outElem: HTMLElement;
|
||||
abstract class PyTerminalBaseClass extends HTMLElement implements Stdio {
|
||||
autoShowOnNextLine: boolean;
|
||||
|
||||
connectedCallback() {
|
||||
// should we use a shadowRoot instead? It looks unnecessarily
|
||||
// complicated to me, but I'm not really sure about the
|
||||
// implications
|
||||
this.outElem = document.createElement('pre');
|
||||
this.outElem.className = 'py-terminal';
|
||||
this.appendChild(this.outElem);
|
||||
isAuto() {
|
||||
return this.hasAttribute('auto');
|
||||
}
|
||||
|
||||
isDocked() {
|
||||
return this.hasAttribute('docked');
|
||||
}
|
||||
|
||||
setupPosition(app: PyScriptApp) {
|
||||
if (this.isAuto()) {
|
||||
this.classList.add('py-terminal-hidden');
|
||||
this.autoShowOnNextLine = true;
|
||||
@@ -93,17 +107,42 @@ function make_PyTerminal(app: PyScriptApp) {
|
||||
this.autoShowOnNextLine = false;
|
||||
}
|
||||
|
||||
if (this.isDocked()) {
|
||||
this.classList.add('py-terminal-docked');
|
||||
}
|
||||
|
||||
logger.info('Registering stdio listener');
|
||||
app.registerStdioListener(this);
|
||||
}
|
||||
|
||||
isAuto() {
|
||||
return this.hasAttribute('auto');
|
||||
abstract stdout_writeline(msg: string): void;
|
||||
abstract stderr_writeline(msg: string): void;
|
||||
}
|
||||
|
||||
function make_PyTerminal_pre(app: PyScriptApp) {
|
||||
/** The <py-terminal> custom element, which automatically register a stdio
|
||||
* listener to capture and display stdout/stderr
|
||||
*/
|
||||
class PyTerminalPre extends PyTerminalBaseClass {
|
||||
outElem: HTMLElement;
|
||||
|
||||
connectedCallback() {
|
||||
// should we use a shadowRoot instead? It looks unnecessarily
|
||||
// complicated to me, but I'm not really sure about the
|
||||
// implications
|
||||
this.outElem = document.createElement('pre');
|
||||
this.outElem.classList.add('py-terminal');
|
||||
this.appendChild(this.outElem);
|
||||
|
||||
this.setupPosition(app);
|
||||
}
|
||||
|
||||
// implementation of the Stdio interface
|
||||
stdout_writeline(msg: string) {
|
||||
this.outElem.innerText += msg + '\n';
|
||||
if (this.isDocked()) {
|
||||
this.scrollTop = this.scrollHeight;
|
||||
}
|
||||
if (this.autoShowOnNextLine) {
|
||||
this.classList.remove('py-terminal-hidden');
|
||||
this.autoShowOnNextLine = false;
|
||||
@@ -116,5 +155,121 @@ function make_PyTerminal(app: PyScriptApp) {
|
||||
// end of the Stdio interface
|
||||
}
|
||||
|
||||
return PyTerminal;
|
||||
return PyTerminalPre;
|
||||
}
|
||||
|
||||
declare const Terminal: typeof TerminalType;
|
||||
|
||||
function make_PyTerminal_xterm(app: PyScriptApp) {
|
||||
/** The <py-terminal> custom element, which automatically register a stdio
|
||||
* listener to capture and display stdout/stderr
|
||||
*/
|
||||
class PyTerminalXterm extends PyTerminalBaseClass {
|
||||
outElem: HTMLDivElement;
|
||||
_moduleResolved: boolean;
|
||||
xtermReady: Promise<TerminalType>;
|
||||
xterm: TerminalType;
|
||||
cachedStdOut: Array<string>;
|
||||
cachedStdErr: Array<string>;
|
||||
_xterm_cdn_base_url = 'https://cdn.jsdelivr.net/npm/xterm@5.1.0';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.cachedStdOut = [];
|
||||
this.cachedStdErr = [];
|
||||
|
||||
// While this is false, store writes to stdout/stderr to a buffer
|
||||
// when the xterm.js is actually ready, we will "replay" those writes
|
||||
// and set this to true
|
||||
this._moduleResolved = false;
|
||||
|
||||
//Required to make xterm appear properly
|
||||
this.style.width = '100%';
|
||||
this.style.height = '100%';
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
//guard against initializing a tag twice
|
||||
if (knownPyTerminalTags.has(this)) return;
|
||||
knownPyTerminalTags.add(this);
|
||||
|
||||
this.outElem = document.createElement('div');
|
||||
//this.outElem.className = 'py-terminal';
|
||||
this.appendChild(this.outElem);
|
||||
|
||||
this.setupPosition(app);
|
||||
|
||||
this.xtermReady = this._setupXterm();
|
||||
await this.xtermReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the xtermjs library from CDN an initialize it.
|
||||
* @private
|
||||
* @returns the associated xterm.js Terminal
|
||||
*/
|
||||
async _setupXterm() {
|
||||
if (this.xterm == undefined) {
|
||||
//need to initialize the Terminal for this element
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
if (globalThis.Terminal == undefined) {
|
||||
//load xterm module from cdn
|
||||
//eslint-disable-next-line
|
||||
//@ts-ignore
|
||||
await import(this._xterm_cdn_base_url + '/lib/xterm.js');
|
||||
|
||||
const cssTag = document.createElement('link');
|
||||
cssTag.type = 'text/css';
|
||||
cssTag.rel = 'stylesheet';
|
||||
cssTag.href = this._xterm_cdn_base_url + '/css/xterm.css';
|
||||
document.head.appendChild(cssTag);
|
||||
}
|
||||
|
||||
//Create xterm, add addons
|
||||
this.xterm = new Terminal({ screenReaderMode: true, cols: 80 });
|
||||
|
||||
// xterm must only 'open' into a visible DOM element
|
||||
// If terminal is still hidden, open during first write
|
||||
if (!this.autoShowOnNextLine) this.xterm.open(this);
|
||||
|
||||
this._moduleResolved = true;
|
||||
|
||||
//Write out any messages output while xterm was loading
|
||||
this.cachedStdOut.forEach((value: string): void => this.stdout_writeline(value));
|
||||
this.cachedStdErr.forEach((value: string): void => this.stderr_writeline(value));
|
||||
} else {
|
||||
this._moduleResolved = true;
|
||||
}
|
||||
return this.xterm;
|
||||
}
|
||||
|
||||
// implementation of the Stdio interface
|
||||
stdout_writeline(msg: string) {
|
||||
if (this._moduleResolved) {
|
||||
this.xterm.writeln(msg);
|
||||
//this.outElem.innerText += msg + '\n';
|
||||
|
||||
if (this.isDocked()) {
|
||||
this.scrollTop = this.scrollHeight;
|
||||
}
|
||||
if (this.autoShowOnNextLine) {
|
||||
this.classList.remove('py-terminal-hidden');
|
||||
this.autoShowOnNextLine = false;
|
||||
this.xterm.open(this);
|
||||
}
|
||||
} else {
|
||||
//if xtermjs not loaded, cache messages
|
||||
this.cachedStdOut.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
stderr_writeline(msg: string) {
|
||||
this.stdout_writeline(msg);
|
||||
}
|
||||
// end of the Stdio interface
|
||||
}
|
||||
|
||||
return PyTerminalXterm;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class MyPlugin(Plugin):
|
||||
console.log(f"configuration received: {config}")
|
||||
|
||||
def afterStartup(self, interpreter):
|
||||
console.log(f"interpreter received: {interpreter}")
|
||||
console.log("interpreter received:", interpreter)
|
||||
|
||||
|
||||
plugin = MyPlugin("py-markdown")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import html
|
||||
|
||||
from pyscript import Plugin, js
|
||||
import js
|
||||
from pyscript import Plugin
|
||||
|
||||
js.console.warn(
|
||||
"WARNING: This plugin is still in a very experimental phase and will likely change"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { $ } from 'basic-devtools';
|
||||
|
||||
import type { AppConfig } from '../pyconfig';
|
||||
import type { UserError } from '../exceptions';
|
||||
import { showWarning } from '../utils';
|
||||
@@ -22,7 +24,9 @@ export class SplashscreenPlugin extends Plugin {
|
||||
autoclose: boolean;
|
||||
enabled: boolean;
|
||||
|
||||
configure(config: AppConfig) {
|
||||
configure(
|
||||
config: AppConfig & { splashscreen?: { autoclose?: boolean; enabled?: boolean }; autoclose_loader?: boolean },
|
||||
) {
|
||||
// the officially supported setting is config.splashscreen.autoclose,
|
||||
// but we still also support the old config.autoclose_loader (with a
|
||||
// deprecation warning)
|
||||
@@ -40,7 +44,7 @@ export class SplashscreenPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
beforeLaunch(config: AppConfig) {
|
||||
beforeLaunch(_config: AppConfig) {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
@@ -50,18 +54,18 @@ export class SplashscreenPlugin extends Plugin {
|
||||
this.elem = <PySplashscreen>document.createElement('py-splashscreen');
|
||||
document.body.append(this.elem);
|
||||
document.addEventListener('py-status-message', (e: CustomEvent) => {
|
||||
const msg = e.detail;
|
||||
const msg = e.detail as string;
|
||||
this.elem.log(msg);
|
||||
});
|
||||
}
|
||||
|
||||
afterStartup(interpreter: InterpreterClient) {
|
||||
afterStartup(_interpreter: InterpreterClient) {
|
||||
if (this.autoclose && this.enabled) {
|
||||
this.elem.close();
|
||||
}
|
||||
}
|
||||
|
||||
onUserError(error: UserError) {
|
||||
onUserError(_error: UserError) {
|
||||
if (this.elem !== undefined && this.enabled) {
|
||||
// Remove the splashscreen so users can see the banner better
|
||||
this.elem.close();
|
||||
@@ -90,8 +94,8 @@ export class PySplashscreen extends HTMLElement {
|
||||
</div>
|
||||
</div>`;
|
||||
this.mount_name = this.id.split('-').join('_');
|
||||
this.operation = document.getElementById('pyscript-operation');
|
||||
this.details = document.getElementById('pyscript-operation-details');
|
||||
this.operation = $('#pyscript-operation', document) as HTMLElement;
|
||||
this.details = $('#pyscript-operation-details', document) as HTMLElement;
|
||||
}
|
||||
|
||||
log(msg: string) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { $ } from 'basic-devtools';
|
||||
|
||||
import { Plugin } from '../plugin';
|
||||
import { TargetedStdio, StdioMultiplexer } from '../stdio';
|
||||
import type { InterpreterClient } from '../interpreter_client';
|
||||
import { createSingularWarning } from '../utils';
|
||||
import { make_PyScript } from '../components/pyscript';
|
||||
import { InterpreterClient } from '../interpreter_client';
|
||||
import { pyDisplay } from '../pyexec';
|
||||
import { make_PyRepl } from '../components/pyrepl';
|
||||
|
||||
type PyScriptTag = InstanceType<ReturnType<typeof make_PyScript>>;
|
||||
|
||||
@@ -58,4 +63,71 @@ export class StdioDirector extends Plugin {
|
||||
options.pyScriptTag.stderr_manager = null;
|
||||
}
|
||||
}
|
||||
|
||||
beforePyReplExec(options: {
|
||||
interpreter: InterpreterClient;
|
||||
src: string;
|
||||
outEl: HTMLElement;
|
||||
pyReplTag: InstanceType<ReturnType<typeof make_PyRepl>>;
|
||||
}): void {
|
||||
//Handle 'output-mode' attribute (removed in PR #881/f9194cc8, restored here)
|
||||
//If output-mode == 'append', don't clear target tag before writing
|
||||
if (options.pyReplTag.getAttribute('output-mode') != 'append') {
|
||||
options.outEl.innerHTML = '';
|
||||
}
|
||||
|
||||
// Handle 'output' attribute; defaults to writing stdout to the existing outEl
|
||||
// If 'output' attribute is used, the DOM element with the specified ID receives
|
||||
// -both- sys.stdout and sys.stderr
|
||||
let output_targeted_io: TargetedStdio;
|
||||
if (options.pyReplTag.hasAttribute('output')) {
|
||||
output_targeted_io = new TargetedStdio(options.pyReplTag, 'output', true, true);
|
||||
} else {
|
||||
output_targeted_io = new TargetedStdio(options.pyReplTag.outDiv, 'id', true, true);
|
||||
}
|
||||
options.pyReplTag.stdout_manager = output_targeted_io;
|
||||
this._stdioMultiplexer.addListener(output_targeted_io);
|
||||
|
||||
//Handle 'stderr' attribute;
|
||||
if (options.pyReplTag.hasAttribute('stderr')) {
|
||||
const stderr_targeted_io = new TargetedStdio(options.pyReplTag, 'stderr', false, true);
|
||||
options.pyReplTag.stderr_manager = stderr_targeted_io;
|
||||
this._stdioMultiplexer.addListener(stderr_targeted_io);
|
||||
}
|
||||
}
|
||||
|
||||
async afterPyReplExec(options: {
|
||||
interpreter: InterpreterClient;
|
||||
src: string;
|
||||
outEl: HTMLElement;
|
||||
pyReplTag: InstanceType<ReturnType<typeof make_PyRepl>>;
|
||||
result: any;
|
||||
}): Promise<void> {
|
||||
// display the value of the last-evaluated expression in the REPL
|
||||
if (options.result !== undefined) {
|
||||
const outputId: string | undefined = options.pyReplTag.getAttribute('output');
|
||||
if (outputId) {
|
||||
// 'output' attribute also used as location to send
|
||||
// result of REPL
|
||||
if ($('#' + outputId, document)) {
|
||||
await pyDisplay(options.interpreter, options.result, { target: outputId });
|
||||
} else {
|
||||
//no matching element on page
|
||||
createSingularWarning(`output = "${outputId}" does not match the id of any element on the page.`);
|
||||
}
|
||||
} else {
|
||||
// 'otuput atribuite not provided
|
||||
await pyDisplay(options.interpreter, options.result, { target: options.outEl.id });
|
||||
}
|
||||
}
|
||||
|
||||
if (options.pyReplTag.stdout_manager != null) {
|
||||
this._stdioMultiplexer.removeListener(options.pyReplTag.stdout_manager);
|
||||
options.pyReplTag.stdout_manager = null;
|
||||
}
|
||||
if (options.pyReplTag.stderr_manager != null) {
|
||||
this._stdioMultiplexer.removeListener(options.pyReplTag.stderr_manager);
|
||||
options.pyReplTag.stderr_manager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import toml from 'toml-j0.4';
|
||||
import toml from '@hoodmane/toml-j0.4';
|
||||
import { getLogger } from './logger';
|
||||
import { version } from './version';
|
||||
import { getAttribute, readTextFromPath, htmlDecode, createDeprecationWarning } from './utils';
|
||||
import { readTextFromPath, htmlDecode, createDeprecationWarning } from './utils';
|
||||
import { UserError, ErrorCode } from './exceptions';
|
||||
|
||||
const logger = getLogger('py-config');
|
||||
@@ -22,6 +22,7 @@ export interface AppConfig extends Record<string, any> {
|
||||
fetch?: FetchConfig[];
|
||||
plugins?: string[];
|
||||
pyscript?: PyScriptMetadata;
|
||||
execution_thread?: string; // "main" or "worker"
|
||||
}
|
||||
|
||||
export type FetchConfig = {
|
||||
@@ -42,19 +43,19 @@ export type PyScriptMetadata = {
|
||||
time?: string;
|
||||
};
|
||||
|
||||
const allKeys = {
|
||||
string: ['name', 'description', 'version', 'type', 'author_name', 'author_email', 'license'],
|
||||
const allKeys = Object.entries({
|
||||
string: ['name', 'description', 'version', 'type', 'author_name', 'author_email', 'license', 'execution_thread'],
|
||||
number: ['schema_version'],
|
||||
array: ['runtimes', 'interpreters', 'packages', 'fetch', 'plugins'],
|
||||
};
|
||||
});
|
||||
|
||||
export const defaultConfig: AppConfig = {
|
||||
schema_version: 1,
|
||||
type: 'app',
|
||||
interpreters: [
|
||||
{
|
||||
src: 'https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.js',
|
||||
name: 'pyodide-0.22.1',
|
||||
src: 'https://cdn.jsdelivr.net/pyodide/v0.23.2/full/pyodide.js',
|
||||
name: 'pyodide-0.23.2',
|
||||
lang: 'python',
|
||||
},
|
||||
],
|
||||
@@ -63,6 +64,7 @@ export const defaultConfig: AppConfig = {
|
||||
packages: [],
|
||||
fetch: [],
|
||||
plugins: [],
|
||||
execution_thread: 'main',
|
||||
};
|
||||
|
||||
export function loadConfigFromElement(el: Element): AppConfig {
|
||||
@@ -72,7 +74,7 @@ export function loadConfigFromElement(el: Element): AppConfig {
|
||||
srcConfig = {};
|
||||
inlineConfig = {};
|
||||
} else {
|
||||
const configType = getAttribute(el, 'type') || 'toml';
|
||||
const configType = el.getAttribute('type') || 'toml';
|
||||
srcConfig = extractFromSrc(el, configType);
|
||||
inlineConfig = extractFromInline(el, configType);
|
||||
}
|
||||
@@ -86,7 +88,7 @@ export function loadConfigFromElement(el: Element): AppConfig {
|
||||
}
|
||||
|
||||
function extractFromSrc(el: Element, configType: string) {
|
||||
const src = getAttribute(el, 'src');
|
||||
const src = el.getAttribute('src');
|
||||
if (src) {
|
||||
logger.info('loading ', src);
|
||||
return validateConfig(readTextFromPath(src), configType);
|
||||
@@ -122,8 +124,7 @@ function mergeConfig(inlineConfig: AppConfig, externalConfig: AppConfig): AppCon
|
||||
} else {
|
||||
let merged: AppConfig = {};
|
||||
|
||||
for (const keyType in allKeys) {
|
||||
const keys: string[] = allKeys[keyType];
|
||||
for (const [keyType, keys] of allKeys) {
|
||||
keys.forEach(function (item: string) {
|
||||
if (keyType === 'boolean') {
|
||||
merged[item] =
|
||||
@@ -143,7 +144,7 @@ function mergeConfig(inlineConfig: AppConfig, externalConfig: AppConfig): AppCon
|
||||
}
|
||||
}
|
||||
|
||||
function parseConfig(configText: string, configType = 'toml') {
|
||||
function parseConfig(configText: string, configType = 'toml'): AppConfig {
|
||||
if (configType === 'toml') {
|
||||
// TOML parser is soft and can parse even JSON strings, this additional check prevents it.
|
||||
if (configText.trim()[0] === '{') {
|
||||
@@ -153,8 +154,9 @@ function parseConfig(configText: string, configType = 'toml') {
|
||||
);
|
||||
}
|
||||
try {
|
||||
return toml.parse(configText);
|
||||
} catch (err) {
|
||||
return toml.parse(configText) as AppConfig;
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
const errMessage: string = err.toString();
|
||||
|
||||
throw new UserError(
|
||||
@@ -164,8 +166,9 @@ function parseConfig(configText: string, configType = 'toml') {
|
||||
}
|
||||
} else if (configType === 'json') {
|
||||
try {
|
||||
return JSON.parse(configText);
|
||||
} catch (err) {
|
||||
return JSON.parse(configText) as AppConfig;
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
const errMessage: string = err.toString();
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
@@ -185,13 +188,12 @@ function validateConfig(configText: string, configType = 'toml') {
|
||||
|
||||
const finalConfig: AppConfig = {};
|
||||
|
||||
for (const keyType in allKeys) {
|
||||
const keys: string[] = allKeys[keyType];
|
||||
for (const [keyType, keys] of allKeys) {
|
||||
keys.forEach(function (item: string) {
|
||||
if (validateParamInConfig(item, keyType, config)) {
|
||||
if (item === 'interpreters') {
|
||||
finalConfig[item] = [];
|
||||
const interpreters = config[item] as InterpreterConfig[];
|
||||
const interpreters = config[item];
|
||||
interpreters.forEach(function (eachInterpreter: InterpreterConfig) {
|
||||
const interpreterConfig: InterpreterConfig = {};
|
||||
for (const eachInterpreterParam in eachInterpreter) {
|
||||
@@ -214,7 +216,7 @@ function validateConfig(configText: string, configType = 'toml') {
|
||||
'',
|
||||
);
|
||||
finalConfig['interpreters'] = [];
|
||||
const interpreters = config[item] as InterpreterConfig[];
|
||||
const interpreters = config[item];
|
||||
interpreters.forEach(function (eachInterpreter: InterpreterConfig) {
|
||||
const interpreterConfig: InterpreterConfig = {};
|
||||
for (const eachInterpreterParam in eachInterpreter) {
|
||||
@@ -226,7 +228,7 @@ function validateConfig(configText: string, configType = 'toml') {
|
||||
});
|
||||
} else if (item === 'fetch') {
|
||||
finalConfig[item] = [];
|
||||
const fetchList = config[item] as FetchConfig[];
|
||||
const fetchList = config[item];
|
||||
fetchList.forEach(function (eachFetch: FetchConfig) {
|
||||
const eachFetchConfig: FetchConfig = {};
|
||||
for (const eachFetchConfigParam in eachFetch) {
|
||||
@@ -237,6 +239,15 @@ function validateConfig(configText: string, configType = 'toml') {
|
||||
}
|
||||
finalConfig[item].push(eachFetchConfig);
|
||||
});
|
||||
} else if (item == 'execution_thread') {
|
||||
const value = config[item];
|
||||
if (value !== 'main' && value !== 'worker') {
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
`"${value}" is not a valid value for the property "execution_thread". The only valid values are "main" and "worker"`,
|
||||
);
|
||||
}
|
||||
finalConfig[item] = value;
|
||||
} else {
|
||||
finalConfig[item] = config[item];
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ import { getLogger } from './logger';
|
||||
import { ensureUniqueId } from './utils';
|
||||
import { UserError, ErrorCode } from './exceptions';
|
||||
import { InterpreterClient } from './interpreter_client';
|
||||
import type { PyProxyCallable } from 'pyodide';
|
||||
|
||||
const logger = getLogger('pyexec');
|
||||
|
||||
export async function pyExec(interpreter: InterpreterClient, pysrc: string, outElem: HTMLElement) {
|
||||
//This is pyscript.py
|
||||
const pyscript_py = interpreter._remote.interface.pyimport('pyscript');
|
||||
export async function pyExec(
|
||||
interpreter: InterpreterClient,
|
||||
pysrc: string,
|
||||
outElem: HTMLElement,
|
||||
): Promise<{ result: any }> {
|
||||
ensureUniqueId(outElem);
|
||||
pyscript_py.set_current_display_target(outElem.id);
|
||||
try {
|
||||
try {
|
||||
if (pyscript_py.uses_top_level_await(pysrc)) {
|
||||
throw new UserError(
|
||||
if (await interpreter._remote.pyscript_internal.uses_top_level_await(pysrc)) {
|
||||
const err = new UserError(
|
||||
ErrorCode.TOP_LEVEL_AWAIT,
|
||||
'The use of top-level "await", "async for", and ' +
|
||||
'"async with" has been removed.' +
|
||||
@@ -21,9 +21,14 @@ export async function pyExec(interpreter: InterpreterClient, pysrc: string, outE
|
||||
'your code and schedule it using asyncio.ensure_future() or similar.' +
|
||||
'\nSee https://docs.pyscript.net/latest/guides/asyncio.html for more information.',
|
||||
);
|
||||
displayPyException(err, outElem);
|
||||
return { result: undefined };
|
||||
}
|
||||
return await interpreter.run(pysrc);
|
||||
} catch (err) {
|
||||
|
||||
try {
|
||||
return await interpreter.run(pysrc, outElem.id);
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
// XXX: currently we display exceptions in the same position as
|
||||
// the output. But we probably need a better way to do that,
|
||||
// e.g. allowing plugins to intercept exceptions and display them
|
||||
@@ -31,10 +36,6 @@ export async function pyExec(interpreter: InterpreterClient, pysrc: string, outE
|
||||
displayPyException(err, outElem);
|
||||
return { result: undefined };
|
||||
}
|
||||
} finally {
|
||||
pyscript_py.set_current_display_target(undefined);
|
||||
pyscript_py.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,16 +45,16 @@ export async function pyExec(interpreter: InterpreterClient, pysrc: string, outE
|
||||
* pyDisplay(interpreter, obj);
|
||||
* pyDisplay(interpreter, obj, { target: targetID });
|
||||
*/
|
||||
export function pyDisplay(interpreter: InterpreterClient, obj: any, kwargs: object) {
|
||||
const display = interpreter.globals.get('display');
|
||||
if (kwargs === undefined) display(obj);
|
||||
else {
|
||||
display.callKwargs(obj, kwargs);
|
||||
export async function pyDisplay(interpreter: InterpreterClient, obj: any, kwargs: { [k: string]: any } = {}) {
|
||||
const display = (await interpreter.globals.get('display')) as PyProxyCallable;
|
||||
try {
|
||||
await display.callKwargs(obj, kwargs);
|
||||
} finally {
|
||||
display.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export function displayPyException(err: any, errElem: HTMLElement) {
|
||||
//addClasses(errElem, ['py-error'])
|
||||
export function displayPyException(err: Error, errElem: HTMLElement) {
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'py-error';
|
||||
|
||||
@@ -65,8 +66,8 @@ export function displayPyException(err: any, errElem: HTMLElement) {
|
||||
} else {
|
||||
// this is very likely a normal JS exception. The best we can do is to
|
||||
// display it as is.
|
||||
logger.error('Non-python exception:\n' + err);
|
||||
pre.innerText = err;
|
||||
logger.error('Non-python exception:\n' + err.toString());
|
||||
pre.innerText = err.toString();
|
||||
}
|
||||
errElem.appendChild(pre);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user