mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 10:47:35 -05:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b41cfb7b60 | ||
|
|
1c675307e1 | ||
|
|
ac56f82c6d | ||
|
|
2ac5ca79d7 | ||
|
|
cb9ee6f7e2 | ||
|
|
9abaef33bd | ||
|
|
320a537db2 | ||
|
|
9b775ce015 | ||
|
|
66f72eda1e | ||
|
|
39ca29749c | ||
|
|
85da548447 | ||
|
|
9985787e4b | ||
|
|
18ec6ce775 | ||
|
|
ed6d0136b8 | ||
|
|
e7216d26e7 | ||
|
|
d1a0d8ea98 | ||
|
|
04222b0d03 | ||
|
|
8ec3381789 | ||
|
|
9bd4737708 | ||
|
|
c49cb9231b | ||
|
|
d1d1c5740f | ||
|
|
1a05ea5fd2 | ||
|
|
5b4e8527da | ||
|
|
83c2afeaf1 | ||
|
|
643b76479f | ||
|
|
cf92996071 | ||
|
|
c653296821 | ||
|
|
44cd6273ba | ||
|
|
d7d2dfb383 | ||
|
|
2d5cf096e0 | ||
|
|
6ee8217593 | ||
|
|
6d45728787 | ||
|
|
65954a627e | ||
|
|
2f1b764251 | ||
|
|
1fb6cddd70 | ||
|
|
239add4e20 | ||
|
|
4e4ac56729 | ||
|
|
1447cb3094 | ||
|
|
2f3659b676 | ||
|
|
910c666319 | ||
|
|
eee2f64c1d | ||
|
|
d080246a0f | ||
|
|
98c0f5e50d | ||
|
|
a1268f1aa2 | ||
|
|
69b8884045 | ||
|
|
df1d699fe6 | ||
|
|
84f197b657 | ||
|
|
5bed5ede52 | ||
|
|
f6d5cf06c8 | ||
|
|
30c6c830ae | ||
|
|
d7084f7f55 | ||
|
|
a87d2b3fea | ||
|
|
81a26363a3 | ||
|
|
53e945201d | ||
|
|
181d276c8b |
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -11,7 +11,9 @@ body:
|
|||||||
|
|
||||||
There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration!
|
There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration!
|
||||||
|
|
||||||
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists
|
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists.
|
||||||
|
|
||||||
|
Finally, if you are opening **a bug report related to PyScript.com** please [use this repository instead](https://github.com/anaconda/pyscript-dot-com-issues/issues/new/choose).
|
||||||
|
|
||||||
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
|
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
|
|||||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*" # Group all Actions updates into a single larger pull request
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
6
.github/workflows/prepare-release.yml
vendored
6
.github/workflows/prepare-release.yml
vendored
@@ -17,12 +17,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
run: zip -r -q ./build.zip ./dist
|
run: zip -r -q ./build.zip ./dist
|
||||||
|
|
||||||
- name: Prepare Release
|
- name: Prepare Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
8
.github/workflows/publish-release.yml
vendored
8
.github/workflows/publish-release.yml
vendored
@@ -19,12 +19,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -46,6 +46,10 @@ jobs:
|
|||||||
working-directory: .
|
working-directory: .
|
||||||
run: sed 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' ./public/index.html > ./pyscript.core/dist/index.html
|
run: sed 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' ./public/index.html > ./pyscript.core/dist/index.html
|
||||||
|
|
||||||
|
- name: Generate release.tar from snapshot and put it in dist/
|
||||||
|
working-directory: .
|
||||||
|
run: tar -cvf ../release.tar * && mv ../release.tar .
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/publish-snapshot.yml
vendored
4
.github/workflows/publish-snapshot.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/publish-unstable.yml
vendored
4
.github/workflows/publish-unstable.yml
vendored
@@ -24,12 +24,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
|
|||||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -37,12 +37,12 @@ jobs:
|
|||||||
run: git log --graph -3
|
run: git log --graph -3
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: setup Miniconda
|
- name: setup Miniconda
|
||||||
uses: conda-incubator/setup-miniconda@v2
|
uses: conda-incubator/setup-miniconda@v3
|
||||||
|
|
||||||
- name: Create and activate virtual environment
|
- name: Create and activate virtual environment
|
||||||
run: |
|
run: |
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
make test-integration
|
make test-integration
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: pyscript
|
name: pyscript
|
||||||
path: |
|
path: |
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
with:
|
with:
|
||||||
name: test_results
|
name: test_results
|
||||||
|
|||||||
2
.github/workflows/test_report.yml
vendored
2
.github/workflows/test_report.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
report:
|
report:
|
||||||
runs-on: ubuntu-latest-8core
|
runs-on: ubuntu-latest-8core
|
||||||
steps:
|
steps:
|
||||||
- uses: dorny/test-reporter@v1.6.0
|
- uses: dorny/test-reporter@v1.9.0
|
||||||
with:
|
with:
|
||||||
artifact: test_results
|
artifact: test_results
|
||||||
name: Test reports
|
name: Test reports
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -142,6 +142,7 @@ coverage/
|
|||||||
test_results
|
test_results
|
||||||
|
|
||||||
# @pyscript/core npm artifacts
|
# @pyscript/core npm artifacts
|
||||||
|
pyscript.core/test-results/*
|
||||||
pyscript.core/core.*
|
pyscript.core/core.*
|
||||||
pyscript.core/dist
|
pyscript.core/dist
|
||||||
pyscript.core/dist.zip
|
pyscript.core/dist.zip
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ ci:
|
|||||||
default_stages: [commit]
|
default_stages: [commit]
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-builtin-literals
|
- id: check-builtin-literals
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
@@ -25,13 +25,13 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.11.0
|
rev: 24.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
|
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
|
||||||
|
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.2.6
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell # See 'pyproject.toml' for args
|
- id: codespell # See 'pyproject.toml' for args
|
||||||
exclude: \.js\.map$
|
exclude: \.js\.map$
|
||||||
@@ -46,7 +46,7 @@ repos:
|
|||||||
args: [--tab-width, "4"]
|
args: [--tab-width, "4"]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: 5.12.0
|
rev: 5.13.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
|||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
|
## 2024.05.21
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- `py-editor` run buttons now display a spinner when disabled, which occurs when the editor is running code.
|
||||||
|
|
||||||
## 2023.05.01
|
## 2023.05.01
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
104
CONTRIBUTING.md
104
CONTRIBUTING.md
@@ -59,9 +59,9 @@ If you would like to contribute to PyScript, but you aren't sure where to begin,
|
|||||||
|
|
||||||
## Setting up your local environment and developing
|
## Setting up your local environment and developing
|
||||||
|
|
||||||
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://pyscript.github.io/docs/latest/development/setting-up-environment.html) will help you get started.
|
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://docs.pyscript.net/latest/contributing/#set-up-your-development-environment) will help you get started.
|
||||||
|
|
||||||
You can also read about PyScript's [development process](https://pyscript.github.io/docs/latest/development/developing.html) to learn how to contribute code to PyScript, how to run tests and what's the PR etiquette of the community!
|
You can also read about PyScript's [development process](https://docs.pyscript.net/latest/developers/) to learn how to contribute code to PyScript, how to run tests and what's the PR etiquette of the community!
|
||||||
|
|
||||||
## License terms for contributions
|
## License terms for contributions
|
||||||
|
|
||||||
@@ -79,3 +79,103 @@ The Project abides by the Organization's [trademark policy](https://github.com/p
|
|||||||
|
|
||||||
Part of MVG-0.1-beta.
|
Part of MVG-0.1-beta.
|
||||||
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||||
|
|
||||||
|
# Quick guide to pytest
|
||||||
|
|
||||||
|
We make heavy usage of pytest. Here is a quick guide and collection of useful options:
|
||||||
|
|
||||||
|
- To run all tests in the current directory and subdirectories: pytest
|
||||||
|
|
||||||
|
- To run tests in a specific directory or file: pytest path/to/dir/test_foo.py
|
||||||
|
|
||||||
|
- -s: disables output capturing
|
||||||
|
|
||||||
|
- --pdb: in case of exception, enter a (Pdb) prompt so that you can inspect what went wrong.
|
||||||
|
|
||||||
|
- -v: verbose mode
|
||||||
|
|
||||||
|
- -x: stop the execution as soon as one test fails
|
||||||
|
|
||||||
|
- -k foo: run only the tests whose full name contains foo
|
||||||
|
|
||||||
|
- -k 'foo and bar'
|
||||||
|
|
||||||
|
- -k 'foo and not bar'
|
||||||
|
|
||||||
|
## Running integration tests under pytest
|
||||||
|
|
||||||
|
make test is useful to run all the tests, but during the development is useful to have more control on how tests are run. The following guide assumes that you are in the directory pyscriptjs/tests/integration/.
|
||||||
|
|
||||||
|
### To run all the integration tests, single or multi core
|
||||||
|
|
||||||
|
$ pytest -xv
|
||||||
|
...
|
||||||
|
|
||||||
|
test_00_support.py::TestSupport::test_basic[chromium] PASSED [ 0%]
|
||||||
|
test_00_support.py::TestSupport::test_console[chromium] PASSED [ 1%]
|
||||||
|
test_00_support.py::TestSupport::test_check_js_errors_simple[chromium] PASSED [ 2%]
|
||||||
|
test_00_support.py::TestSupport::test_check_js_errors_expected[chromium] PASSED [ 3%]
|
||||||
|
test_00_support.py::TestSupport::test_check_js_errors_expected_but_didnt_raise[chromium] PASSED [ 4%]
|
||||||
|
test_00_support.py::TestSupport::test_check_js_errors_multiple[chromium] PASSED [ 5%]
|
||||||
|
...
|
||||||
|
|
||||||
|
-x means "stop at the first failure". -v means "verbose", so that you can see all the test names one by one. We try to keep tests in a reasonable order, from most basic to most complex. This way, if you introduced some bug in very basic things, you will notice immediately.
|
||||||
|
|
||||||
|
If you have the pytest-xdist plugin installed, you can run all the integration tests on 4 cores in parallel:
|
||||||
|
|
||||||
|
$ pytest -n 4
|
||||||
|
|
||||||
|
### To run a single test, headless
|
||||||
|
|
||||||
|
$ pytest test_01_basic.py -k test_pyscript_hello -s
|
||||||
|
...
|
||||||
|
[ 0.00 page.goto ] pyscript_hello.html
|
||||||
|
[ 0.01 request ] 200 - fake_server - http://fake_server/pyscript_hello.html
|
||||||
|
...
|
||||||
|
[ 0.17 console.info ] [py-loader] Downloading pyodide-x.y.z...
|
||||||
|
[ 0.18 request ] 200 - CACHED - https://cdn.jsdelivr.net/pyodide/vx.y.z/full/pyodide.js
|
||||||
|
...
|
||||||
|
[ 3.59 console.info ] [pyscript/main] PyScript page fully initialized
|
||||||
|
[ 3.60 console.log ] hello pyscript
|
||||||
|
|
||||||
|
-k selects tests by pattern matching as described above. -s instructs pytest to show the output to the terminal instead of capturing it. In the output you can see various useful things, including network requests and JS console messages.
|
||||||
|
|
||||||
|
### To run a single test, headed
|
||||||
|
|
||||||
|
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed
|
||||||
|
...
|
||||||
|
|
||||||
|
Same as above, but with --headed the browser is shown in a window, and you can interact with it. The browser uses a fake server, which means that HTTP requests are cached.
|
||||||
|
|
||||||
|
Unfortunately, in this mode source maps does not seem to work, and you cannot debug the original typescript source code. This seems to be a bug in playwright, for which we have a workaround:
|
||||||
|
|
||||||
|
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed --no-fake-server
|
||||||
|
...
|
||||||
|
|
||||||
|
As the name implies, -no-fake-server disables the fake server: HTTP requests are not cached, but source-level debugging works.
|
||||||
|
|
||||||
|
Finally:
|
||||||
|
|
||||||
|
$ pytest test_01_basic.py -k test_pyscript_hello -s --dev
|
||||||
|
...
|
||||||
|
|
||||||
|
--dev implies --headed --no-fake-server. In addition, it also automatically open chrome dev tools.
|
||||||
|
|
||||||
|
### To run only main thread or worker tests
|
||||||
|
|
||||||
|
By default, we run each test twice: one with execution_thread = "main" and one with execution_thread = "worker". If you want to run only half of them, you can use -m:
|
||||||
|
|
||||||
|
$ pytest -m main # run only the tests in the main thread
|
||||||
|
$ pytest -m worker # ron only the tests in the web worker
|
||||||
|
|
||||||
|
## Fake server, HTTP cache
|
||||||
|
|
||||||
|
By default, our test machinery uses a playwright router which intercepts and cache HTTP requests, so that for example you don't have to download pyodide again and again. This also enables the possibility of running tests in parallel on multiple cores.
|
||||||
|
|
||||||
|
The cache is stored using the pytest-cache plugin, which means that it survives across sessions.
|
||||||
|
|
||||||
|
If you want to temporarily disable the cache, the easiest thing is to use --no-fake-server, which bypasses it completely.
|
||||||
|
|
||||||
|
If you want to clear the cache, you can use the special option --clear-http-cache:
|
||||||
|
|
||||||
|
NOTE: this works only if you are inside tests/integration, or if you explicitly specify tests/integration from the command line. This is due to how pytest decides to search for and load the various conftest.py.
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -38,11 +38,11 @@ To try PyScript, import the appropriate pyscript files into the `<head>` tag of
|
|||||||
<head>
|
<head>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://pyscript.net/releases/2023.11.2/core.css"
|
href="https://pyscript.net/releases/2024.5.2/core.css"
|
||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
type="module"
|
type="module"
|
||||||
src="https://pyscript.net/releases/2023.11.2/core.js"
|
src="https://pyscript.net/releases/2024.5.2/core.js"
|
||||||
></script>
|
></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -67,10 +67,29 @@ Check out the [official docs](https://docs.pyscript.net/) for more detailed docu
|
|||||||
|
|
||||||
## How to Contribute
|
## How to Contribute
|
||||||
|
|
||||||
Read the [contributing guide](CONTRIBUTING.md) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
|
Read the [contributing guide](https://docs.pyscript.net/latest/contributing/) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
|
||||||
|
|
||||||
Check out the [developing process](https://pyscript.github.io/docs/latest/contributing) documentation for more information on how to setup your development environment.
|
Check out the [developing process](https://docs.pyscript.net/latest/developers/) documentation for more information on how to setup your development environment.
|
||||||
|
|
||||||
## Governance
|
## Governance
|
||||||
|
|
||||||
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.
|
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
To cut a new release of PyScript simply
|
||||||
|
[add a new release](https://github.com/pyscript/pyscript/releases) while
|
||||||
|
remembering to write a comprehensive changelog. A [GitHub action](https://github.com/pyscript/pyscript/blob/main/.github/workflows/publish-release.yml)
|
||||||
|
will kick in and ensure the release is described and deployed to a URL with the
|
||||||
|
pattern: https://pyscript.net/releases/YYYY.M.v/ (year/month/version - as per
|
||||||
|
our [CalVer](https://calver.org/) versioning scheme).
|
||||||
|
|
||||||
|
Then, the following three separate repositories need updating:
|
||||||
|
|
||||||
|
- [Documentation](https://github.com/pyscript/docs) - Change the `version.json`
|
||||||
|
file in the root of the directory and then `node version-update.js`.
|
||||||
|
- [Homepage](https://github.com/pyscript/pyscript.net) - Ensure the version
|
||||||
|
referenced in `index.html` is the latest version.
|
||||||
|
- [PSDC](https://pyscript.com) - Use discord or Anaconda Slack (if you work at
|
||||||
|
Anaconda) to let the PSDC team know there's a new version, so they can update
|
||||||
|
their project templates.
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2021: true,
|
|
||||||
},
|
|
||||||
extends: "eslint:recommended",
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
files: [".eslintrc.{js,cjs}"],
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: "script",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
ignorePatterns: ["3rd-party"],
|
|
||||||
rules: {
|
|
||||||
"no-implicit-globals": ["error"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
.eslintrc.cjs
|
.eslintrc.cjs
|
||||||
|
eslint.config.mjs
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
node_modules/
|
node_modules/
|
||||||
rollup/
|
rollup/
|
||||||
test/
|
test/
|
||||||
tests/
|
tests/
|
||||||
|
test-results/
|
||||||
src/stdlib/_pyscript
|
src/stdlib/_pyscript
|
||||||
src/stdlib/pyscript.py
|
src/stdlib/pyscript.py
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|||||||
22
pyscript.core/eslint.config.mjs
Normal file
22
pyscript.core/eslint.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import globals from "globals";
|
||||||
|
import js from "@eslint/js";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
ignores: ["**/3rd-party/"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.es2021,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-implicit-globals": ["error"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
1447
pyscript.core/package-lock.json
generated
1447
pyscript.core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pyscript/core",
|
"name": "@pyscript/core",
|
||||||
"version": "0.3.23",
|
"version": "0.4.42",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "PyScript",
|
"description": "PyScript",
|
||||||
"module": "./index.js",
|
"module": "./index.js",
|
||||||
@@ -20,13 +20,14 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"server": "npx static-handler --coi .",
|
"server": "npx static-handler --coi .",
|
||||||
"build": "npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy",
|
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy",
|
||||||
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
||||||
"build:plugins": "node rollup/plugins.cjs",
|
"build:plugins": "node rollup/plugins.cjs",
|
||||||
"build:stdlib": "node rollup/stdlib.cjs",
|
"build:stdlib": "node rollup/stdlib.cjs",
|
||||||
"build:3rd-party": "node rollup/3rd-party.cjs",
|
"build:3rd-party": "node rollup/3rd-party.cjs",
|
||||||
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||||
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/ || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/mpy.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||||
|
"test:ws": "bun test/ws/index.js & playwright test test/ws.spec.js",
|
||||||
"dev": "node dev.cjs",
|
"dev": "node dev.cjs",
|
||||||
"release": "npm run build && npm run zip",
|
"release": "npm run build && npm run zip",
|
||||||
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
|
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
|
||||||
@@ -42,31 +43,33 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ungap/with-resolvers": "^0.1.0",
|
"@ungap/with-resolvers": "^0.1.0",
|
||||||
"basic-devtools": "^0.1.6",
|
"basic-devtools": "^0.1.6",
|
||||||
"polyscript": "^0.6.18",
|
"polyscript": "^0.12.14",
|
||||||
"sticky-module": "^0.1.1",
|
"sticky-module": "^0.1.1",
|
||||||
"to-json-callback": "^0.1.1",
|
"to-json-callback": "^0.1.1",
|
||||||
"type-checked-collections": "^0.1.7"
|
"type-checked-collections": "^0.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/commands": "^6.3.3",
|
"@codemirror/commands": "^6.6.0",
|
||||||
"@codemirror/lang-python": "^6.1.3",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/language": "^6.10.0",
|
"@codemirror/language": "^6.10.2",
|
||||||
"@codemirror/state": "^6.4.0",
|
"@codemirror/state": "^6.4.1",
|
||||||
"@codemirror/view": "^6.23.1",
|
"@codemirror/view": "^6.27.0",
|
||||||
"@playwright/test": "^1.41.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@rollup/plugin-commonjs": "^25.0.7",
|
"@rollup/plugin-commonjs": "^25.0.8",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@webreflection/toml-j0.4": "^1.1.3",
|
"@webreflection/toml-j0.4": "^1.1.3",
|
||||||
"@xterm/addon-fit": "^0.9.0-beta.1",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"chokidar": "^3.5.3",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
|
"bun": "^1.1.12",
|
||||||
|
"chokidar": "^3.6.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^9.4.0",
|
||||||
"rollup": "^4.9.6",
|
"rollup": "^4.18.0",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-string": "^3.0.0",
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"static-handler": "^0.4.3",
|
"static-handler": "^0.4.3",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.4.5",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-readline": "^1.1.1"
|
"xterm-readline": "^1.1.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ const modules = {
|
|||||||
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||||
b.text(),
|
b.text(),
|
||||||
),
|
),
|
||||||
|
"xterm_addon-web-links.js": fetch(
|
||||||
|
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||||
|
).then((b) => b.text()),
|
||||||
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
||||||
(b) => b.text(),
|
(b) => b.text(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -42,3 +42,34 @@ mpy-config {
|
|||||||
.mpy-editor-run-button:disabled {
|
.mpy-editor-run-button:disabled {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spinner {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.py-editor-run-button:disabled > *,
|
||||||
|
.mpy-editor-run-button:disabled > * {
|
||||||
|
display: none; /* hide all the child elements of the run button when it is disabled */
|
||||||
|
}
|
||||||
|
.py-editor-run-button:disabled,
|
||||||
|
.mpy-editor-run-button:disabled {
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
.py-editor-run-button:disabled::before,
|
||||||
|
.mpy-editor-run-button:disabled::before {
|
||||||
|
content: "";
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 100%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: -23px; /* hardcoded value to center the spinner on the run button */
|
||||||
|
margin-left: -26px; /* hardcoded value to center the spinner on the run button */
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #aaa;
|
||||||
|
border-top-color: #000;
|
||||||
|
background-color: #fff;
|
||||||
|
animation: spinner 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,15 +24,51 @@ import sync from "./sync.js";
|
|||||||
import bootstrapNodeAndPlugins from "./plugins-helper.js";
|
import bootstrapNodeAndPlugins from "./plugins-helper.js";
|
||||||
import { ErrorCode } from "./exceptions.js";
|
import { ErrorCode } from "./exceptions.js";
|
||||||
import { robustFetch as fetch, getText } from "./fetch.js";
|
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||||
import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
|
import {
|
||||||
|
hooks,
|
||||||
|
main,
|
||||||
|
worker,
|
||||||
|
codeFor,
|
||||||
|
createFunction,
|
||||||
|
inputFailure,
|
||||||
|
} from "./hooks.js";
|
||||||
|
|
||||||
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
|
export { stdlib, optional, inputFailure };
|
||||||
|
|
||||||
// generic helper to disambiguate between custom element and script
|
// generic helper to disambiguate between custom element and script
|
||||||
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
||||||
|
|
||||||
|
// Used to create either Pyodide or MicroPython workers
|
||||||
|
// with the PyScript module available within the code
|
||||||
|
const [PyWorker, MPWorker] = [...TYPES.entries()].map(
|
||||||
|
([TYPE, interpreter]) =>
|
||||||
|
/**
|
||||||
|
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||||
|
* @param {string} file the python file to run ina worker.
|
||||||
|
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
||||||
|
* @returns {Promise<Worker & {sync: object}>}
|
||||||
|
*/
|
||||||
|
async function PyScriptWorker(file, options) {
|
||||||
|
await configs.get(TYPE).plugins;
|
||||||
|
const xworker = XWorker.call(
|
||||||
|
new Hook(null, hooked.get(TYPE)),
|
||||||
|
file,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
type: interpreter,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assign(xworker.sync, sync);
|
||||||
|
return xworker.ready;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// avoid multiple initialization of the same library
|
// avoid multiple initialization of the same library
|
||||||
const [
|
const [
|
||||||
{
|
{
|
||||||
PyWorker: exportedPyWorker,
|
PyWorker: exportedPyWorker,
|
||||||
|
MPWorker: exportedMPWorker,
|
||||||
hooks: exportedHooks,
|
hooks: exportedHooks,
|
||||||
config: exportedConfig,
|
config: exportedConfig,
|
||||||
whenDefined: exportedWhenDefined,
|
whenDefined: exportedWhenDefined,
|
||||||
@@ -40,6 +76,7 @@ const [
|
|||||||
alreadyLive,
|
alreadyLive,
|
||||||
] = stickyModule("@pyscript/core", {
|
] = stickyModule("@pyscript/core", {
|
||||||
PyWorker,
|
PyWorker,
|
||||||
|
MPWorker,
|
||||||
hooks,
|
hooks,
|
||||||
config: {},
|
config: {},
|
||||||
whenDefined,
|
whenDefined,
|
||||||
@@ -48,11 +85,15 @@ const [
|
|||||||
export {
|
export {
|
||||||
TYPES,
|
TYPES,
|
||||||
exportedPyWorker as PyWorker,
|
exportedPyWorker as PyWorker,
|
||||||
|
exportedMPWorker as MPWorker,
|
||||||
exportedHooks as hooks,
|
exportedHooks as hooks,
|
||||||
exportedConfig as config,
|
exportedConfig as config,
|
||||||
exportedWhenDefined as whenDefined,
|
exportedWhenDefined as whenDefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const offline_interpreter = (config) =>
|
||||||
|
config?.interpreter && new URL(config.interpreter, location.href).href;
|
||||||
|
|
||||||
const hooked = new Map();
|
const hooked = new Map();
|
||||||
|
|
||||||
for (const [TYPE, interpreter] of TYPES) {
|
for (const [TYPE, interpreter] of TYPES) {
|
||||||
@@ -116,6 +157,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
// enrich the Python env with some JS utility for main
|
// enrich the Python env with some JS utility for main
|
||||||
interpreter.registerJsModule("_pyscript", {
|
interpreter.registerJsModule("_pyscript", {
|
||||||
PyWorker,
|
PyWorker,
|
||||||
|
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
||||||
get target() {
|
get target() {
|
||||||
return isScript(currentElement)
|
return isScript(currentElement)
|
||||||
? currentElement.target.id
|
? currentElement.target.id
|
||||||
@@ -137,7 +179,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
// specific main and worker hooks
|
// specific main and worker hooks
|
||||||
const hooks = {
|
const hooks = {
|
||||||
main: {
|
main: {
|
||||||
...codeFor(main),
|
...codeFor(main, TYPE),
|
||||||
async onReady(wrap, element) {
|
async onReady(wrap, element) {
|
||||||
registerModule(wrap);
|
registerModule(wrap);
|
||||||
|
|
||||||
@@ -234,7 +276,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
worker: {
|
worker: {
|
||||||
...codeFor(worker),
|
...codeFor(worker, TYPE),
|
||||||
// these are lazy getters that returns a composition
|
// these are lazy getters that returns a composition
|
||||||
// of the current hooks or undefined, if no hook is present
|
// of the current hooks or undefined, if no hook is present
|
||||||
get onReady() {
|
get onReady() {
|
||||||
@@ -263,7 +305,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
interpreter,
|
interpreter,
|
||||||
hooks,
|
hooks,
|
||||||
env: `${TYPE}-script`,
|
env: `${TYPE}-script`,
|
||||||
version: config?.interpreter,
|
version: offline_interpreter(config),
|
||||||
onerror(error, element) {
|
onerror(error, element) {
|
||||||
errors.set(element, error);
|
errors.set(element, error);
|
||||||
},
|
},
|
||||||
@@ -314,24 +356,3 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
// export the used config without allowing leaks through it
|
// export the used config without allowing leaks through it
|
||||||
exportedConfig[TYPE] = structuredClone(config);
|
exportedConfig[TYPE] = structuredClone(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
|
||||||
* @param {string} file the python file to run ina worker.
|
|
||||||
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
|
||||||
* @returns {Worker & {sync: ProxyHandler<object>}}
|
|
||||||
*/
|
|
||||||
function PyWorker(file, options) {
|
|
||||||
const hooks = hooked.get("py");
|
|
||||||
// this propagates pyscript worker hooks without needing a pyscript
|
|
||||||
// bootstrap + it passes arguments and it defaults to `pyodide`
|
|
||||||
// as the interpreter to use in the worker, as all hooks assume that
|
|
||||||
// and as `pyodide` is the only default interpreter that can deal with
|
|
||||||
// all the features we need to deliver pyscript out there.
|
|
||||||
const xworker = XWorker.call(new Hook(null, hooks), file, {
|
|
||||||
type: "pyodide",
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
assign(xworker.sync, sync);
|
|
||||||
return xworker;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { FetchError, ErrorCode } from "./exceptions.js";
|
import { FetchError, ErrorCode } from "./exceptions.js";
|
||||||
import { getText } from "polyscript/exports";
|
|
||||||
|
|
||||||
export { getText };
|
/**
|
||||||
|
* @param {Response} response
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getText = (response) => response.text();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a fetch wrapper that handles any non 200 responses and throws a
|
* This is a fetch wrapper that handles any non 200 responses and throws a
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { typedSet } from "type-checked-collections";
|
|||||||
import { dedent } from "polyscript/exports";
|
import { dedent } from "polyscript/exports";
|
||||||
import toJSONCallback from "to-json-callback";
|
import toJSONCallback from "to-json-callback";
|
||||||
|
|
||||||
import stdlib from "./stdlib.js";
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
|
|
||||||
export const main = (name) => hooks.main[name];
|
export const main = (name) => hooks.main[name];
|
||||||
export const worker = (name) => hooks.worker[name];
|
export const worker = (name) => hooks.worker[name];
|
||||||
@@ -15,10 +15,11 @@ const code = (hooks, branch, key, lib) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const codeFor = (branch) => {
|
export const codeFor = (branch, type) => {
|
||||||
|
const pylib = type === "mpy" ? stdlib.replace(optional, "") : stdlib;
|
||||||
const hooks = {};
|
const hooks = {};
|
||||||
code(hooks, branch, `codeBeforeRun`, stdlib);
|
code(hooks, branch, `codeBeforeRun`, pylib);
|
||||||
code(hooks, branch, `codeBeforeRunAsync`, stdlib);
|
code(hooks, branch, `codeBeforeRunAsync`, pylib);
|
||||||
code(hooks, branch, `codeAfterRun`);
|
code(hooks, branch, `codeAfterRun`);
|
||||||
code(hooks, branch, `codeAfterRunAsync`);
|
code(hooks, branch, `codeAfterRunAsync`);
|
||||||
return hooks;
|
return hooks;
|
||||||
@@ -45,7 +46,7 @@ export const createFunction = (self, name) => {
|
|||||||
const SetFunction = typedSet({ typeof: "function" });
|
const SetFunction = typedSet({ typeof: "function" });
|
||||||
const SetString = typedSet({ typeof: "string" });
|
const SetString = typedSet({ typeof: "string" });
|
||||||
|
|
||||||
const inputFailure = `
|
export const inputFailure = `
|
||||||
import builtins
|
import builtins
|
||||||
def input(prompt=""):
|
def input(prompt=""):
|
||||||
raise Exception("\\n ".join([
|
raise Exception("\\n ".join([
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// PyScript py-editor plugin
|
// PyScript py-editor plugin
|
||||||
import { Hook, XWorker, dedent } from "polyscript/exports";
|
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||||
import { TYPES } from "../core.js";
|
import { TYPES, offline_interpreter, stdlib } from "../core.js";
|
||||||
|
|
||||||
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
const RUN_BUTTON = `<svg style="height: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>`;
|
||||||
|
|
||||||
@@ -8,13 +8,15 @@ let id = 0;
|
|||||||
const getID = (type) => `${type}-editor-${id++}`;
|
const getID = (type) => `${type}-editor-${id++}`;
|
||||||
|
|
||||||
const envs = new Map();
|
const envs = new Map();
|
||||||
|
const configs = new Map();
|
||||||
|
|
||||||
const hooks = {
|
const hooks = {
|
||||||
worker: {
|
worker: {
|
||||||
|
codeBeforeRun: () => stdlib,
|
||||||
// works on both Pyodide and MicroPython
|
// works on both Pyodide and MicroPython
|
||||||
onReady: ({ runAsync, io }, { sync }) => {
|
onReady: ({ runAsync, io }, { sync }) => {
|
||||||
io.stdout = (line) => sync.write(line);
|
io.stdout = io.buffered(sync.write);
|
||||||
io.stderr = (line) => sync.writeErr(line);
|
io.stderr = io.buffered(sync.writeErr);
|
||||||
sync.revoke();
|
sync.revoke();
|
||||||
sync.runAsync = runAsync;
|
sync.runAsync = runAsync;
|
||||||
},
|
},
|
||||||
@@ -23,15 +25,29 @@ const hooks = {
|
|||||||
|
|
||||||
async function execute({ currentTarget }) {
|
async function execute({ currentTarget }) {
|
||||||
const { env, pySrc, outDiv } = this;
|
const { env, pySrc, outDiv } = this;
|
||||||
|
const hasRunButton = !!currentTarget;
|
||||||
|
|
||||||
|
if (hasRunButton) {
|
||||||
currentTarget.disabled = true;
|
currentTarget.disabled = true;
|
||||||
outDiv.innerHTML = "";
|
outDiv.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
if (!envs.has(env)) {
|
if (!envs.has(env)) {
|
||||||
const srcLink = URL.createObjectURL(new Blob([""]));
|
const srcLink = URL.createObjectURL(new Blob([""]));
|
||||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, {
|
const details = { type: this.interpreter };
|
||||||
type: this.interpreter,
|
const { config } = this;
|
||||||
});
|
if (config) {
|
||||||
|
details.configURL = config;
|
||||||
|
const { parse } = config.endsWith(".toml")
|
||||||
|
? await import(/* webpackIgnore: true */ "../3rd-party/toml.js")
|
||||||
|
: JSON;
|
||||||
|
details.config = parse(await fetch(config).then((r) => r.text()));
|
||||||
|
details.version = offline_interpreter(details.config);
|
||||||
|
} else {
|
||||||
|
details.config = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||||
|
|
||||||
const { sync } = xworker;
|
const { sync } = xworker;
|
||||||
const { promise, resolve } = Promise.withResolvers();
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
@@ -44,23 +60,27 @@ async function execute({ currentTarget }) {
|
|||||||
|
|
||||||
// wait for the env then set the target div
|
// wait for the env then set the target div
|
||||||
// before executing the current code
|
// before executing the current code
|
||||||
envs.get(env).then((xworker) => {
|
return envs.get(env).then((xworker) => {
|
||||||
xworker.onerror = ({ error }) => {
|
xworker.onerror = ({ error }) => {
|
||||||
|
if (hasRunButton) {
|
||||||
outDiv.innerHTML += `<span style='color:red'>${
|
outDiv.innerHTML += `<span style='color:red'>${
|
||||||
error.message || error
|
error.message || error
|
||||||
}</span>\n`;
|
}</span>\n`;
|
||||||
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
const enable = () => {
|
const enable = () => {
|
||||||
currentTarget.disabled = false;
|
if (hasRunButton) currentTarget.disabled = false;
|
||||||
};
|
};
|
||||||
const { sync } = xworker;
|
const { sync } = xworker;
|
||||||
sync.write = (str) => {
|
sync.write = (str) => {
|
||||||
outDiv.innerText += `${str}\n`;
|
if (hasRunButton) outDiv.innerText += `${str}\n`;
|
||||||
};
|
};
|
||||||
sync.writeErr = (str) => {
|
sync.writeErr = (str) => {
|
||||||
|
if (hasRunButton) {
|
||||||
outDiv.innerHTML += `<span style='color:red'>${str}</span>\n`;
|
outDiv.innerHTML += `<span style='color:red'>${str}</span>\n`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
sync.runAsync(pySrc).then(enable, enable);
|
sync.runAsync(pySrc).then(enable, enable);
|
||||||
});
|
});
|
||||||
@@ -120,7 +140,6 @@ const init = async (script, type, interpreter) => {
|
|||||||
{ keymap },
|
{ keymap },
|
||||||
{ defaultKeymap },
|
{ defaultKeymap },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
// TODO: find a way to actually produce these bundles locally
|
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
||||||
import(
|
import(
|
||||||
@@ -131,9 +150,90 @@ const init = async (script, type, interpreter) => {
|
|||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const selector = script.getAttribute("target");
|
let isSetup = script.hasAttribute("setup");
|
||||||
|
const hasConfig = script.hasAttribute("config");
|
||||||
|
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
|
||||||
|
|
||||||
|
if (hasConfig && configs.has(env)) {
|
||||||
|
throw new SyntaxError(
|
||||||
|
configs.get(env)
|
||||||
|
? `duplicated config for env: ${env}`
|
||||||
|
: `unable to add a config to the env: ${env}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
configs.set(env, hasConfig);
|
||||||
|
|
||||||
|
let source = script.src
|
||||||
|
? await fetch(script.src).then((b) => b.text())
|
||||||
|
: script.textContent;
|
||||||
|
const context = {
|
||||||
|
interpreter,
|
||||||
|
env,
|
||||||
|
config:
|
||||||
|
hasConfig &&
|
||||||
|
new URL(script.getAttribute("config"), location.href).href,
|
||||||
|
get pySrc() {
|
||||||
|
return isSetup ? source : editor.state.doc.toString();
|
||||||
|
},
|
||||||
|
get outDiv() {
|
||||||
|
return isSetup ? null : outDiv;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let target;
|
let target;
|
||||||
|
defineProperties(script, {
|
||||||
|
target: { get: () => target },
|
||||||
|
code: {
|
||||||
|
get: () => context.pySrc,
|
||||||
|
set: (insert) => {
|
||||||
|
if (isSetup) return;
|
||||||
|
editor.update([
|
||||||
|
editor.state.update({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: editor.state.doc.length,
|
||||||
|
insert,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
process: {
|
||||||
|
/**
|
||||||
|
* Simulate a setup node overriding the source to evaluate.
|
||||||
|
* @param {string} code the Python code to evaluate.
|
||||||
|
* @returns {Promise<...>} fulfill once code has been evaluated.
|
||||||
|
*/
|
||||||
|
value(code) {
|
||||||
|
const wasSetup = isSetup;
|
||||||
|
const wasSource = source;
|
||||||
|
isSetup = true;
|
||||||
|
source = code;
|
||||||
|
const restore = () => {
|
||||||
|
isSetup = wasSetup;
|
||||||
|
source = wasSource;
|
||||||
|
};
|
||||||
|
return execute
|
||||||
|
.call(context, { currentTarget: null })
|
||||||
|
.then(restore, restore);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const notify = () => {
|
||||||
|
const event = new Event(`${type}-editor`, { bubbles: true });
|
||||||
|
script.dispatchEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSetup) {
|
||||||
|
await execute.call(context, { currentTarget: null });
|
||||||
|
notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selector = script.getAttribute("target");
|
||||||
|
|
||||||
if (selector) {
|
if (selector) {
|
||||||
target =
|
target =
|
||||||
document.getElementById(selector) ||
|
document.getElementById(selector) ||
|
||||||
@@ -149,18 +249,6 @@ const init = async (script, type, interpreter) => {
|
|||||||
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
|
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
|
||||||
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
|
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
|
||||||
|
|
||||||
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
|
|
||||||
const context = {
|
|
||||||
interpreter,
|
|
||||||
env,
|
|
||||||
get pySrc() {
|
|
||||||
return editor.state.doc.toString();
|
|
||||||
},
|
|
||||||
get outDiv() {
|
|
||||||
return outDiv;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
|
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
|
||||||
const listener = execute.bind(context);
|
const listener = execute.bind(context);
|
||||||
const [boxDiv, outDiv] = makeBoxDiv(listener, type);
|
const [boxDiv, outDiv] = makeBoxDiv(listener, type);
|
||||||
@@ -195,6 +283,7 @@ const init = async (script, type, interpreter) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
editor.focus();
|
editor.focus();
|
||||||
|
notify();
|
||||||
};
|
};
|
||||||
|
|
||||||
// avoid too greedy MutationObserver operations at distance
|
// avoid too greedy MutationObserver operations at distance
|
||||||
@@ -210,7 +299,7 @@ const resetTimeout = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// triggered both ASAP on the living DOM and via MutationObserver later
|
// triggered both ASAP on the living DOM and via MutationObserver later
|
||||||
const pyEditor = async () => {
|
const pyEditor = () => {
|
||||||
if (timeout) return;
|
if (timeout) return;
|
||||||
timeout = setTimeout(resetTimeout, 250);
|
timeout = setTimeout(resetTimeout, 250);
|
||||||
for (const [type, interpreter] of TYPES) {
|
for (const [type, interpreter] of TYPES) {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// PyScript py-terminal plugin
|
// PyScript py-terminal plugin
|
||||||
import { TYPES, hooks } from "../core.js";
|
import { TYPES } from "../core.js";
|
||||||
import { notify } from "./error.js";
|
import { notify } from "./error.js";
|
||||||
import { defineProperty } from "polyscript/exports";
|
import { customObserver } from "polyscript/exports";
|
||||||
|
|
||||||
const SELECTOR = [...TYPES.keys()]
|
// will contain all valid selectors
|
||||||
.map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`)
|
const SELECTORS = [];
|
||||||
.join(",");
|
|
||||||
|
// avoid processing same elements twice
|
||||||
|
const processed = new WeakSet();
|
||||||
|
|
||||||
// show the error on main and
|
// show the error on main and
|
||||||
// stops the module from keep executing
|
// stops the module from keep executing
|
||||||
@@ -14,60 +16,16 @@ const notifyAndThrow = (message) => {
|
|||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const notParsedYet = (script) => !bootstrapped.has(script);
|
|
||||||
|
|
||||||
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
||||||
|
|
||||||
const bootstrapped = new WeakSet();
|
|
||||||
|
|
||||||
let addStyle = true;
|
let addStyle = true;
|
||||||
|
|
||||||
// this callback will be serialized as string and it never needs
|
for (const type of TYPES.keys()) {
|
||||||
// to be invoked multiple times. Each xworker here is bootstrapped
|
const selector = `script[type="${type}"][terminal],${type}-script[terminal]`;
|
||||||
// only once thanks to the `sync.is_pyterminal()` check.
|
SELECTORS.push(selector);
|
||||||
const workerReady = ({ interpreter, io, run }, { sync }) => {
|
customObserver.set(selector, async (element) => {
|
||||||
if (!sync.is_pyterminal()) return;
|
// we currently support only one terminal on main as in "classic"
|
||||||
|
const terminals = document.querySelectorAll(SELECTORS.join(","));
|
||||||
// in workers it's always safe to grab the polyscript currentScript
|
|
||||||
run("from polyscript.currentScript import terminal as __terminal__");
|
|
||||||
|
|
||||||
// This part is inevitably duplicated as external scope
|
|
||||||
// can't be reached by workers out of the box.
|
|
||||||
// The detail is that here we use sync though, not readline.
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let data = "";
|
|
||||||
const generic = {
|
|
||||||
isatty: true,
|
|
||||||
write(buffer) {
|
|
||||||
data = decoder.decode(buffer);
|
|
||||||
sync.pyterminal_write(data);
|
|
||||||
return buffer.length;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
interpreter.setStdout(generic);
|
|
||||||
interpreter.setStderr(generic);
|
|
||||||
interpreter.setStdin({
|
|
||||||
isatty: true,
|
|
||||||
stdin: () => sync.pyterminal_read(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
io.stderr = (error) => {
|
|
||||||
sync.pyterminal_write(`${error.message || error}\n`);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const pyTerminal = async () => {
|
|
||||||
const terminals = document.querySelectorAll(SELECTOR);
|
|
||||||
|
|
||||||
const unknown = [].filter.call(terminals, notParsedYet);
|
|
||||||
|
|
||||||
// no results will look further for runtime nodes
|
|
||||||
if (!unknown.length) return;
|
|
||||||
// early flag elements as known to avoid concurrent
|
|
||||||
// MutationObserver invokes of this async handler
|
|
||||||
else unknown.forEach(bootstrapped.add, bootstrapped);
|
|
||||||
|
|
||||||
// we currently support only one terminal as in "classic"
|
|
||||||
if ([].filter.call(terminals, onceOnMain).length > 1)
|
if ([].filter.call(terminals, onceOnMain).length > 1)
|
||||||
notifyAndThrow("You can use at most 1 main terminal");
|
notifyAndThrow("You can use at most 1 main terminal");
|
||||||
|
|
||||||
@@ -82,125 +40,21 @@ const pyTerminal = async () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// lazy load these only when a valid terminal is found
|
if (processed.has(element)) return;
|
||||||
const [{ Terminal }, { Readline }, { FitAddon }] = await Promise.all([
|
processed.add(element);
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/xterm.js"),
|
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/xterm-readline.js"),
|
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (const element of unknown) {
|
const bootstrap = (module) => module.default(element);
|
||||||
// hopefully to be removed in the near future!
|
|
||||||
if (element.matches('script[type="mpy"],mpy-script'))
|
|
||||||
notifyAndThrow("Unsupported terminal.");
|
|
||||||
|
|
||||||
const readline = new Readline();
|
// we can't be smart with template literals for the dynamic import
|
||||||
|
// or bundlers are incapable of producing multiple files around
|
||||||
// common main thread initialization for both worker
|
if (type === "mpy") {
|
||||||
// or main case, bootstrapping the terminal on its target
|
await import(/* webpackIgnore: true */ "./py-terminal/mpy.js").then(
|
||||||
const init = (options) => {
|
bootstrap,
|
||||||
let target = element;
|
);
|
||||||
const selector = element.getAttribute("target");
|
|
||||||
if (selector) {
|
|
||||||
target =
|
|
||||||
document.getElementById(selector) ||
|
|
||||||
document.querySelector(selector);
|
|
||||||
if (!target) throw new Error(`Unknown target ${selector}`);
|
|
||||||
} else {
|
} else {
|
||||||
target = document.createElement("py-terminal");
|
await import(/* webpackIgnore: true */ "./py-terminal/py.js").then(
|
||||||
target.style.display = "block";
|
bootstrap,
|
||||||
element.after(target);
|
);
|
||||||
}
|
}
|
||||||
const terminal = new Terminal({
|
|
||||||
theme: {
|
|
||||||
background: "#191A19",
|
|
||||||
foreground: "#F5F2E7",
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
const fitAddon = new FitAddon();
|
|
||||||
terminal.loadAddon(fitAddon);
|
|
||||||
terminal.loadAddon(readline);
|
|
||||||
terminal.open(target);
|
|
||||||
fitAddon.fit();
|
|
||||||
terminal.focus();
|
|
||||||
defineProperty(element, "terminal", { value: terminal });
|
|
||||||
return terminal;
|
|
||||||
};
|
|
||||||
|
|
||||||
// branch logic for the worker
|
|
||||||
if (element.hasAttribute("worker")) {
|
|
||||||
// add a hook on the main thread to setup all sync helpers
|
|
||||||
// also bootstrapping the XTerm target on main *BUT* ...
|
|
||||||
hooks.main.onWorker.add(function worker(_, xworker) {
|
|
||||||
// ... as multiple workers will add multiple callbacks
|
|
||||||
// be sure no xworker is ever initialized twice!
|
|
||||||
if (bootstrapped.has(xworker)) return;
|
|
||||||
bootstrapped.add(xworker);
|
|
||||||
|
|
||||||
// still cleanup this callback for future scripts/workers
|
|
||||||
hooks.main.onWorker.delete(worker);
|
|
||||||
|
|
||||||
init({
|
|
||||||
disableStdin: false,
|
|
||||||
cursorBlink: true,
|
|
||||||
cursorStyle: "block",
|
|
||||||
});
|
|
||||||
|
|
||||||
xworker.sync.is_pyterminal = () => true;
|
|
||||||
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
|
||||||
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
|
||||||
});
|
|
||||||
|
|
||||||
// setup remote thread JS/Python code for whenever the
|
|
||||||
// worker is ready to become a terminal
|
|
||||||
hooks.worker.onReady.add(workerReady);
|
|
||||||
} else {
|
|
||||||
// in the main case, just bootstrap XTerm without
|
|
||||||
// allowing any input as that's not possible / awkward
|
|
||||||
hooks.main.onReady.add(function main({ interpreter, io, run }) {
|
|
||||||
console.warn("py-terminal is read only on main thread");
|
|
||||||
hooks.main.onReady.delete(main);
|
|
||||||
|
|
||||||
// on main, it's easy to trash and clean the current terminal
|
|
||||||
globalThis.__py_terminal__ = init({
|
|
||||||
disableStdin: true,
|
|
||||||
cursorBlink: false,
|
|
||||||
cursorStyle: "underline",
|
|
||||||
});
|
|
||||||
run("from js import __py_terminal__ as __terminal__");
|
|
||||||
delete globalThis.__py_terminal__;
|
|
||||||
|
|
||||||
// This part is inevitably duplicated as external scope
|
|
||||||
// can't be reached by workers out of the box.
|
|
||||||
// The detail is that here we use readline here, not sync.
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let data = "";
|
|
||||||
const generic = {
|
|
||||||
isatty: true,
|
|
||||||
write(buffer) {
|
|
||||||
data = decoder.decode(buffer);
|
|
||||||
readline.write(data);
|
|
||||||
return buffer.length;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
interpreter.setStdout(generic);
|
|
||||||
interpreter.setStderr(generic);
|
|
||||||
interpreter.setStdin({
|
|
||||||
isatty: true,
|
|
||||||
stdin: () => readline.read(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
io.stderr = (error) => {
|
|
||||||
readline.write(`${error.message || error}\n`);
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mo = new MutationObserver(pyTerminal);
|
|
||||||
mo.observe(document, { childList: true, subtree: true });
|
|
||||||
|
|
||||||
// try to check the current document ASAP
|
|
||||||
export default pyTerminal();
|
|
||||||
|
|||||||
239
pyscript.core/src/plugins/py-terminal/mpy.js
Normal file
239
pyscript.core/src/plugins/py-terminal/mpy.js
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
// PyScript pyodide terminal plugin
|
||||||
|
import { hooks, inputFailure } from "../../core.js";
|
||||||
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
|
||||||
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
|
// this callback will be serialized as string and it never needs
|
||||||
|
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||||
|
// only once thanks to the `sync.is_pyterminal()` check.
|
||||||
|
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||||
|
if (type !== "mpy" || !sync.is_pyterminal()) return;
|
||||||
|
|
||||||
|
const { pyterminal_ready, pyterminal_read, pyterminal_write } = sync;
|
||||||
|
|
||||||
|
interpreter.registerJsModule("_pyscript_input", {
|
||||||
|
input: pyterminal_read,
|
||||||
|
});
|
||||||
|
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from _pyscript_input import input",
|
||||||
|
"from polyscript import currentScript as _",
|
||||||
|
"__terminal__ = _.terminal",
|
||||||
|
"del _",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const missingReturn = new Uint8Array([13]);
|
||||||
|
io.stdout = (buffer) => {
|
||||||
|
if (buffer[0] === 10) pyterminal_write(missingReturn);
|
||||||
|
pyterminal_write(buffer);
|
||||||
|
};
|
||||||
|
io.stderr = (error) => {
|
||||||
|
pyterminal_write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
// tiny shim of the code module with only interact
|
||||||
|
// to bootstrap a REPL like environment
|
||||||
|
interpreter.registerJsModule("code", {
|
||||||
|
interact() {
|
||||||
|
const encoder = new TextEncoderStream();
|
||||||
|
encoder.readable.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write(buffer) {
|
||||||
|
for (const c of buffer) interpreter.replProcessChar(c);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const writer = encoder.writable.getWriter();
|
||||||
|
sync.pyterminal_stream_write = (buffer) => writer.write(buffer);
|
||||||
|
pyterminal_ready();
|
||||||
|
|
||||||
|
interpreter.replInit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (element) => {
|
||||||
|
// lazy load these only when a valid terminal is found
|
||||||
|
const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const terminalOptions = {
|
||||||
|
disableStdin: false,
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: "block",
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream;
|
||||||
|
|
||||||
|
// common main thread initialization for both worker
|
||||||
|
// or main case, bootstrapping the terminal on its target
|
||||||
|
const init = () => {
|
||||||
|
let target = element;
|
||||||
|
const selector = element.getAttribute("target");
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement("py-terminal");
|
||||||
|
target.style.display = "block";
|
||||||
|
element.after(target);
|
||||||
|
}
|
||||||
|
const terminal = new Terminal({
|
||||||
|
theme: {
|
||||||
|
background: "#191A19",
|
||||||
|
foreground: "#F5F2E7",
|
||||||
|
},
|
||||||
|
...terminalOptions,
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(new WebLinksAddon());
|
||||||
|
terminal.open(target);
|
||||||
|
fitAddon.fit();
|
||||||
|
terminal.focus();
|
||||||
|
defineProperties(element, {
|
||||||
|
terminal: { value: terminal },
|
||||||
|
process: {
|
||||||
|
value: async (code) => {
|
||||||
|
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||||
|
await stream.write(`${line}\r`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return terminal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// branch logic for the worker
|
||||||
|
if (element.hasAttribute("worker")) {
|
||||||
|
// add a hook on the main thread to setup all sync helpers
|
||||||
|
// also bootstrapping the XTerm target on main *BUT* ...
|
||||||
|
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||||
|
// ... as multiple workers will add multiple callbacks
|
||||||
|
// be sure no xworker is ever initialized twice!
|
||||||
|
if (bootstrapped.has(xworker)) return;
|
||||||
|
bootstrapped.add(xworker);
|
||||||
|
|
||||||
|
// still cleanup this callback for future scripts/workers
|
||||||
|
hooks.main.onWorker.delete(worker);
|
||||||
|
|
||||||
|
const terminal = init();
|
||||||
|
|
||||||
|
const { sync } = xworker;
|
||||||
|
|
||||||
|
// handle the read mode on input
|
||||||
|
let promisedChunks = null;
|
||||||
|
let readChunks = "";
|
||||||
|
|
||||||
|
sync.is_pyterminal = () => true;
|
||||||
|
|
||||||
|
// put the terminal in a read-only state
|
||||||
|
// frees the worker on \r
|
||||||
|
sync.pyterminal_read = (buffer) => {
|
||||||
|
terminal.write(buffer);
|
||||||
|
promisedChunks = Promise.withResolvers();
|
||||||
|
return promisedChunks.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// write if not reading input
|
||||||
|
sync.pyterminal_write = (buffer) => {
|
||||||
|
if (!promisedChunks) terminal.write(buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// add the onData terminal listener which forwards to the worker
|
||||||
|
// everything typed in a queued char-by-char way
|
||||||
|
sync.pyterminal_ready = () => {
|
||||||
|
let queue = Promise.resolve();
|
||||||
|
stream = {
|
||||||
|
write: (buffer) =>
|
||||||
|
(queue = queue.then(() =>
|
||||||
|
sync.pyterminal_stream_write(buffer),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
terminal.onData((buffer) => {
|
||||||
|
if (promisedChunks) {
|
||||||
|
readChunks += buffer;
|
||||||
|
terminal.write(buffer);
|
||||||
|
if (readChunks.endsWith("\r")) {
|
||||||
|
terminal.write("\n");
|
||||||
|
promisedChunks.resolve(readChunks.slice(0, -1));
|
||||||
|
promisedChunks = null;
|
||||||
|
readChunks = "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stream.write(buffer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup remote thread JS/Python code for whenever the
|
||||||
|
// worker is ready to become a terminal
|
||||||
|
hooks.worker.onReady.add(workerReady);
|
||||||
|
} else {
|
||||||
|
// ⚠️ In an ideal world the inputFailure should never be used on main.
|
||||||
|
// However, Pyodide still can't compete with MicroPython REPL mode
|
||||||
|
// so while it's OK to keep that entry on main as default, we need
|
||||||
|
// to remove it ASAP from `mpy` use cases, otherwise MicroPython would
|
||||||
|
// also throw whenever an `input(...)` is required / digited.
|
||||||
|
hooks.main.codeBeforeRun.delete(inputFailure);
|
||||||
|
|
||||||
|
// in the main case, just bootstrap XTerm without
|
||||||
|
// allowing any input as that's not possible / awkward
|
||||||
|
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||||
|
if (type !== "mpy") return;
|
||||||
|
|
||||||
|
hooks.main.onReady.delete(main);
|
||||||
|
|
||||||
|
const terminal = init();
|
||||||
|
|
||||||
|
const missingReturn = new Uint8Array([13]);
|
||||||
|
io.stdout = (buffer) => {
|
||||||
|
if (buffer[0] === 10) terminal.write(missingReturn);
|
||||||
|
terminal.write(buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// expose the __terminal__ one-off reference
|
||||||
|
globalThis.__py_terminal__ = terminal;
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from js import prompt as input",
|
||||||
|
"from js import __py_terminal__ as __terminal__",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
delete globalThis.__py_terminal__;
|
||||||
|
|
||||||
|
// NOTE: this is NOT the same as the one within
|
||||||
|
// the onWorkerReady callback!
|
||||||
|
interpreter.registerJsModule("code", {
|
||||||
|
interact() {
|
||||||
|
const encoder = new TextEncoderStream();
|
||||||
|
encoder.readable.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write(buffer) {
|
||||||
|
for (const c of buffer)
|
||||||
|
interpreter.replProcessChar(c);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
stream = encoder.writable.getWriter();
|
||||||
|
terminal.onData((buffer) => stream.write(buffer));
|
||||||
|
|
||||||
|
interpreter.replInit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
179
pyscript.core/src/plugins/py-terminal/py.js
Normal file
179
pyscript.core/src/plugins/py-terminal/py.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
// PyScript py-terminal plugin
|
||||||
|
import { hooks } from "../../core.js";
|
||||||
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
|
||||||
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
|
// this callback will be serialized as string and it never needs
|
||||||
|
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||||
|
// only once thanks to the `sync.is_pyterminal()` check.
|
||||||
|
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||||
|
if (type !== "py" || !sync.is_pyterminal()) return;
|
||||||
|
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from polyscript import currentScript as _",
|
||||||
|
"__terminal__ = _.terminal",
|
||||||
|
"del _",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
const { pyterminal_read, pyterminal_write } = sync;
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const generic = {
|
||||||
|
isatty: false,
|
||||||
|
write(buffer) {
|
||||||
|
data = decoder.decode(buffer);
|
||||||
|
pyterminal_write(data);
|
||||||
|
return buffer.length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
io.stderr = (error) => {
|
||||||
|
pyterminal_write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
interpreter.setStdout(generic);
|
||||||
|
interpreter.setStderr(generic);
|
||||||
|
interpreter.setStdin({
|
||||||
|
isatty: false,
|
||||||
|
stdin: () => pyterminal_read(data),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (element) => {
|
||||||
|
// lazy load these only when a valid terminal is found
|
||||||
|
const [{ Terminal }, { Readline }, { FitAddon }, { WebLinksAddon }] =
|
||||||
|
await Promise.all([
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm-readline.js"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const readline = new Readline();
|
||||||
|
|
||||||
|
// common main thread initialization for both worker
|
||||||
|
// or main case, bootstrapping the terminal on its target
|
||||||
|
const init = (options) => {
|
||||||
|
let target = element;
|
||||||
|
const selector = element.getAttribute("target");
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement("py-terminal");
|
||||||
|
target.style.display = "block";
|
||||||
|
element.after(target);
|
||||||
|
}
|
||||||
|
const terminal = new Terminal({
|
||||||
|
theme: {
|
||||||
|
background: "#191A19",
|
||||||
|
foreground: "#F5F2E7",
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(readline);
|
||||||
|
terminal.loadAddon(new WebLinksAddon());
|
||||||
|
terminal.open(target);
|
||||||
|
fitAddon.fit();
|
||||||
|
terminal.focus();
|
||||||
|
defineProperties(element, {
|
||||||
|
terminal: { value: terminal },
|
||||||
|
process: {
|
||||||
|
value: async (code) => {
|
||||||
|
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||||
|
terminal.paste(`${line}`);
|
||||||
|
terminal.write("\r\n");
|
||||||
|
do {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 0),
|
||||||
|
);
|
||||||
|
} while (!readline.activeRead?.resolve);
|
||||||
|
readline.activeRead.resolve(line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return terminal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// branch logic for the worker
|
||||||
|
if (element.hasAttribute("worker")) {
|
||||||
|
// add a hook on the main thread to setup all sync helpers
|
||||||
|
// also bootstrapping the XTerm target on main *BUT* ...
|
||||||
|
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||||
|
// ... as multiple workers will add multiple callbacks
|
||||||
|
// be sure no xworker is ever initialized twice!
|
||||||
|
if (bootstrapped.has(xworker)) return;
|
||||||
|
bootstrapped.add(xworker);
|
||||||
|
|
||||||
|
// still cleanup this callback for future scripts/workers
|
||||||
|
hooks.main.onWorker.delete(worker);
|
||||||
|
|
||||||
|
init({
|
||||||
|
disableStdin: false,
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: "block",
|
||||||
|
});
|
||||||
|
|
||||||
|
xworker.sync.is_pyterminal = () => true;
|
||||||
|
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
||||||
|
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup remote thread JS/Python code for whenever the
|
||||||
|
// worker is ready to become a terminal
|
||||||
|
hooks.worker.onReady.add(workerReady);
|
||||||
|
} else {
|
||||||
|
// in the main case, just bootstrap XTerm without
|
||||||
|
// allowing any input as that's not possible / awkward
|
||||||
|
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||||
|
if (type !== "py") return;
|
||||||
|
|
||||||
|
console.warn("py-terminal is read only on main thread");
|
||||||
|
hooks.main.onReady.delete(main);
|
||||||
|
|
||||||
|
// on main, it's easy to trash and clean the current terminal
|
||||||
|
globalThis.__py_terminal__ = init({
|
||||||
|
disableStdin: true,
|
||||||
|
cursorBlink: false,
|
||||||
|
cursorStyle: "underline",
|
||||||
|
});
|
||||||
|
run("from js import __py_terminal__ as __terminal__");
|
||||||
|
delete globalThis.__py_terminal__;
|
||||||
|
|
||||||
|
io.stderr = (error) => {
|
||||||
|
readline.write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const generic = {
|
||||||
|
isatty: false,
|
||||||
|
write(buffer) {
|
||||||
|
data = decoder.decode(buffer);
|
||||||
|
readline.write(data);
|
||||||
|
return buffer.length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
interpreter.setStdout(generic);
|
||||||
|
interpreter.setStderr(generic);
|
||||||
|
interpreter.setStdin({
|
||||||
|
isatty: false,
|
||||||
|
stdin: () => readline.read(data),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -8,6 +8,27 @@
|
|||||||
|
|
||||||
import pyscript from "./stdlib/pyscript.js";
|
import pyscript from "./stdlib/pyscript.js";
|
||||||
|
|
||||||
|
class Ignore extends Array {
|
||||||
|
#add = false;
|
||||||
|
#paths;
|
||||||
|
#array;
|
||||||
|
constructor(array, ...paths) {
|
||||||
|
super();
|
||||||
|
this.#array = array;
|
||||||
|
this.#paths = paths;
|
||||||
|
}
|
||||||
|
push(...values) {
|
||||||
|
if (this.#add) super.push(...values);
|
||||||
|
return this.#array.push(...values);
|
||||||
|
}
|
||||||
|
path(path) {
|
||||||
|
for (const _path of this.#paths) {
|
||||||
|
// bails out at the first `true` value
|
||||||
|
if ((this.#add = path.startsWith(_path))) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { entries } = Object;
|
const { entries } = Object;
|
||||||
|
|
||||||
const python = [
|
const python = [
|
||||||
@@ -16,16 +37,19 @@ const python = [
|
|||||||
"_path = None",
|
"_path = None",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ignore = new Ignore(python, "-");
|
||||||
|
|
||||||
const write = (base, literal) => {
|
const write = (base, literal) => {
|
||||||
for (const [key, value] of entries(literal)) {
|
for (const [key, value] of entries(literal)) {
|
||||||
python.push(`_path = _Path("${base}/${key}")`);
|
ignore.path(`${base}/${key}`);
|
||||||
|
ignore.push(`_path = _Path("${base}/${key}")`);
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
const code = JSON.stringify(value);
|
const code = JSON.stringify(value);
|
||||||
python.push(`_path.write_text(${code})`);
|
ignore.push(`_path.write_text(${code},encoding="utf-8")`);
|
||||||
} else {
|
} else {
|
||||||
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
|
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
|
||||||
python.push(`if not _os.path.exists("${base}/${key}"):`);
|
ignore.push(`if not _os.path.exists("${base}/${key}"):`);
|
||||||
python.push(" _path.mkdir(parents=True, exist_ok=True)");
|
ignore.push(" _path.mkdir(parents=True, exist_ok=True)");
|
||||||
write(`${base}/${key}`, value);
|
write(`${base}/${key}`, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,4 +66,5 @@ python.push(
|
|||||||
);
|
);
|
||||||
python.push("\n");
|
python.push("\n");
|
||||||
|
|
||||||
export default python.join("\n");
|
export const stdlib = python.join("\n");
|
||||||
|
export const optional = ignore.join("\n");
|
||||||
|
|||||||
@@ -29,16 +29,21 @@
|
|||||||
# pyscript.magic_js. This is the blessed way to access them from pyscript,
|
# pyscript.magic_js. This is the blessed way to access them from pyscript,
|
||||||
# as it works transparently in both the main thread and worker cases.
|
# as it works transparently in both the main thread and worker cases.
|
||||||
|
|
||||||
|
from polyscript import lazy_py_modules as py_import
|
||||||
from pyscript.display import HTML, display
|
from pyscript.display import HTML, display
|
||||||
|
from pyscript.fetch import fetch
|
||||||
from pyscript.magic_js import (
|
from pyscript.magic_js import (
|
||||||
RUNNING_IN_WORKER,
|
RUNNING_IN_WORKER,
|
||||||
PyWorker,
|
PyWorker,
|
||||||
|
config,
|
||||||
current_target,
|
current_target,
|
||||||
document,
|
document,
|
||||||
|
js_import,
|
||||||
js_modules,
|
js_modules,
|
||||||
sync,
|
sync,
|
||||||
window,
|
window,
|
||||||
)
|
)
|
||||||
|
from pyscript.websocket import WebSocket
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pyscript.event_handling import when
|
from pyscript.event_handling import when
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import re
|
|||||||
from pyscript.magic_js import current_target, document, window
|
from pyscript.magic_js import current_target, document, window
|
||||||
|
|
||||||
_MIME_METHODS = {
|
_MIME_METHODS = {
|
||||||
"__repr__": "text/plain",
|
|
||||||
"_repr_html_": "text/html",
|
|
||||||
"_repr_markdown_": "text/markdown",
|
|
||||||
"_repr_svg_": "image/svg+xml",
|
|
||||||
"_repr_pdf_": "application/pdf",
|
|
||||||
"_repr_jpeg_": "image/jpeg",
|
|
||||||
"_repr_png_": "image/png",
|
|
||||||
"_repr_latex": "text/latex",
|
|
||||||
"_repr_json_": "application/json",
|
|
||||||
"_repr_javascript_": "application/javascript",
|
|
||||||
"savefig": "image/png",
|
"savefig": "image/png",
|
||||||
|
"_repr_javascript_": "application/javascript",
|
||||||
|
"_repr_json_": "application/json",
|
||||||
|
"_repr_latex": "text/latex",
|
||||||
|
"_repr_png_": "image/png",
|
||||||
|
"_repr_jpeg_": "image/jpeg",
|
||||||
|
"_repr_pdf_": "application/pdf",
|
||||||
|
"_repr_svg_": "image/svg+xml",
|
||||||
|
"_repr_markdown_": "text/markdown",
|
||||||
|
"_repr_html_": "text/html",
|
||||||
|
"__repr__": "text/plain",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ def _format_mime(obj):
|
|||||||
format_dict = mimebundle
|
format_dict = mimebundle
|
||||||
|
|
||||||
output, not_available = None, []
|
output, not_available = None, []
|
||||||
for method, mime_type in reversed(_MIME_METHODS.items()):
|
for method, mime_type in _MIME_METHODS.items():
|
||||||
if mime_type in format_dict:
|
if mime_type in format_dict:
|
||||||
output = format_dict[mime_type]
|
output = format_dict[mime_type]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -47,13 +47,14 @@ def when(event_type=None, selector=None):
|
|||||||
wrapper = func
|
wrapper = func
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# TODO: this is currently an quick hack to get micropython working but we need
|
# TODO: this is very ugly hack to get micropython working because inspect.signature
|
||||||
# to actually properly replace inspect.signature with something else
|
# doesn't exist, but we need to actually properly replace inspect.signature.
|
||||||
|
# It may be actually better to not try any magic for now and raise the error
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
if "takes 0 positional arguments" in str(e):
|
if "takes" in str(e) and "positional arguments" in str(e):
|
||||||
return func()
|
return func()
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|||||||
87
pyscript.core/src/stdlib/pyscript/fetch.py
Normal file
87
pyscript.core/src/stdlib/pyscript/fetch.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import js
|
||||||
|
from pyscript.util import as_bytearray
|
||||||
|
|
||||||
|
|
||||||
|
### wrap the response to grant Pythonic results
|
||||||
|
class _Response:
|
||||||
|
def __init__(self, response):
|
||||||
|
self._response = response
|
||||||
|
|
||||||
|
# grant access to response.ok and other fields
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self._response, attr)
|
||||||
|
|
||||||
|
# exposed methods with Pythonic results
|
||||||
|
async def arrayBuffer(self):
|
||||||
|
buffer = await self._response.arrayBuffer()
|
||||||
|
# works in Pyodide
|
||||||
|
if hasattr(buffer, "to_py"):
|
||||||
|
return buffer.to_py()
|
||||||
|
# shims in MicroPython
|
||||||
|
return memoryview(as_bytearray(buffer))
|
||||||
|
|
||||||
|
async def blob(self):
|
||||||
|
return await self._response.blob()
|
||||||
|
|
||||||
|
async def bytearray(self):
|
||||||
|
buffer = await self._response.arrayBuffer()
|
||||||
|
return as_bytearray(buffer)
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
return json.loads(await self.text())
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
return await self._response.text()
|
||||||
|
|
||||||
|
|
||||||
|
### allow direct await to _Response methods
|
||||||
|
class _DirectResponse:
|
||||||
|
@staticmethod
|
||||||
|
def setup(promise, response):
|
||||||
|
promise._response = _Response(response)
|
||||||
|
return promise._response
|
||||||
|
|
||||||
|
def __init__(self, promise):
|
||||||
|
self._promise = promise
|
||||||
|
promise._response = None
|
||||||
|
promise.arrayBuffer = self.arrayBuffer
|
||||||
|
promise.blob = self.blob
|
||||||
|
promise.bytearray = self.bytearray
|
||||||
|
promise.json = self.json
|
||||||
|
promise.text = self.text
|
||||||
|
|
||||||
|
async def _response(self):
|
||||||
|
if not self._promise._response:
|
||||||
|
await self._promise
|
||||||
|
return self._promise._response
|
||||||
|
|
||||||
|
async def arrayBuffer(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.arrayBuffer()
|
||||||
|
|
||||||
|
async def blob(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.blob()
|
||||||
|
|
||||||
|
async def bytearray(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.bytearray()
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.text()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url, **kw):
|
||||||
|
# workaround Pyodide / MicroPython dict <-> js conversion
|
||||||
|
options = js.JSON.parse(json.dumps(kw))
|
||||||
|
awaited = lambda response, *args: _DirectResponse.setup(promise, response)
|
||||||
|
promise = js.fetch(url, options).then(awaited)
|
||||||
|
_DirectResponse(promise)
|
||||||
|
return promise
|
||||||
18
pyscript.core/src/stdlib/pyscript/ffi.py
Normal file
18
pyscript.core/src/stdlib/pyscript/ffi.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
try:
|
||||||
|
import js
|
||||||
|
from pyodide.ffi import create_proxy as _cp
|
||||||
|
from pyodide.ffi import to_js as _py_tjs
|
||||||
|
|
||||||
|
from_entries = js.Object.fromEntries
|
||||||
|
|
||||||
|
def _tjs(value, **kw):
|
||||||
|
if not hasattr(kw, "dict_converter"):
|
||||||
|
kw["dict_converter"] = from_entries
|
||||||
|
return _py_tjs(value, **kw)
|
||||||
|
|
||||||
|
except:
|
||||||
|
from jsffi import create_proxy as _cp
|
||||||
|
from jsffi import to_js as _tjs
|
||||||
|
|
||||||
|
create_proxy = _cp
|
||||||
|
to_js = _tjs
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import js as globalThis
|
import js as globalThis
|
||||||
|
from polyscript import config as _config
|
||||||
from polyscript import js_modules
|
from polyscript import js_modules
|
||||||
from pyscript.util import NotSupported
|
from pyscript.util import NotSupported
|
||||||
|
|
||||||
RUNNING_IN_WORKER = not hasattr(globalThis, "document")
|
RUNNING_IN_WORKER = not hasattr(globalThis, "document")
|
||||||
|
|
||||||
|
config = json.loads(globalThis.JSON.stringify(_config))
|
||||||
|
|
||||||
|
|
||||||
# allow `from pyscript.js_modules.xxx import yyy`
|
# allow `from pyscript.js_modules.xxx import yyy`
|
||||||
class JSModule:
|
class JSModule:
|
||||||
@@ -24,16 +28,37 @@ for name in globalThis.Reflect.ownKeys(js_modules):
|
|||||||
sys.modules["pyscript.js_modules"] = js_modules
|
sys.modules["pyscript.js_modules"] = js_modules
|
||||||
|
|
||||||
if RUNNING_IN_WORKER:
|
if RUNNING_IN_WORKER:
|
||||||
import js
|
|
||||||
import polyscript
|
import polyscript
|
||||||
|
|
||||||
PyWorker = NotSupported(
|
PyWorker = NotSupported(
|
||||||
"pyscript.PyWorker",
|
"pyscript.PyWorker",
|
||||||
"pyscript.PyWorker works only when running in the main thread",
|
"pyscript.PyWorker works only when running in the main thread",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
globalThis.SharedArrayBuffer.new(4)
|
||||||
|
import js
|
||||||
|
|
||||||
window = polyscript.xworker.window
|
window = polyscript.xworker.window
|
||||||
document = window.document
|
document = window.document
|
||||||
js.document = document
|
js.document = document
|
||||||
|
# this is the same as js_import on main and it lands modules on main
|
||||||
|
js_import = window.Function(
|
||||||
|
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
|
||||||
|
)()
|
||||||
|
except:
|
||||||
|
globalThis.console.debug("SharedArrayBuffer is not available")
|
||||||
|
# in this scenario none of the utilities would work
|
||||||
|
# as expected so we better export these as NotSupported
|
||||||
|
window = NotSupported(
|
||||||
|
"pyscript.window",
|
||||||
|
"pyscript.window in workers works only via SharedArrayBuffer",
|
||||||
|
)
|
||||||
|
document = NotSupported(
|
||||||
|
"pyscript.document",
|
||||||
|
"pyscript.document in workers works only via SharedArrayBuffer",
|
||||||
|
)
|
||||||
|
|
||||||
sync = polyscript.xworker.sync
|
sync = polyscript.xworker.sync
|
||||||
|
|
||||||
# in workers the display does not have a default ID
|
# in workers the display does not have a default ID
|
||||||
@@ -43,7 +68,7 @@ if RUNNING_IN_WORKER:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
import _pyscript
|
import _pyscript
|
||||||
from _pyscript import PyWorker
|
from _pyscript import PyWorker, js_import
|
||||||
|
|
||||||
window = globalThis
|
window = globalThis
|
||||||
document = globalThis.document
|
document = globalThis.document
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
import js
|
||||||
|
|
||||||
|
|
||||||
|
def as_bytearray(buffer):
|
||||||
|
ui8a = js.Uint8Array.new(buffer)
|
||||||
|
size = ui8a.length
|
||||||
|
ba = bytearray(size)
|
||||||
|
for i in range(0, size):
|
||||||
|
ba[i] = ui8a[i]
|
||||||
|
return ba
|
||||||
|
|
||||||
|
|
||||||
class NotSupported:
|
class NotSupported:
|
||||||
"""
|
"""
|
||||||
Small helper that raises exceptions if you try to get/set any attribute on
|
Small helper that raises exceptions if you try to get/set any attribute on
|
||||||
|
|||||||
67
pyscript.core/src/stdlib/pyscript/websocket.py
Normal file
67
pyscript.core/src/stdlib/pyscript/websocket.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import js
|
||||||
|
from pyscript.util import as_bytearray
|
||||||
|
|
||||||
|
code = "code"
|
||||||
|
protocols = "protocols"
|
||||||
|
reason = "reason"
|
||||||
|
|
||||||
|
|
||||||
|
class EventMessage:
|
||||||
|
def __init__(self, event):
|
||||||
|
self._event = event
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
value = getattr(self._event, attr)
|
||||||
|
|
||||||
|
if attr == "data" and not isinstance(value, str):
|
||||||
|
if hasattr(value, "to_py"):
|
||||||
|
return value.to_py()
|
||||||
|
# shims in MicroPython
|
||||||
|
return memoryview(as_bytearray(value))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocket(object):
|
||||||
|
CONNECTING = 0
|
||||||
|
OPEN = 1
|
||||||
|
CLOSING = 2
|
||||||
|
CLOSED = 3
|
||||||
|
|
||||||
|
def __init__(self, **kw):
|
||||||
|
url = kw["url"]
|
||||||
|
if protocols in kw:
|
||||||
|
socket = js.WebSocket.new(url, kw[protocols])
|
||||||
|
else:
|
||||||
|
socket = js.WebSocket.new(url)
|
||||||
|
object.__setattr__(self, "_ws", socket)
|
||||||
|
|
||||||
|
for t in ["onclose", "onerror", "onmessage", "onopen"]:
|
||||||
|
if t in kw:
|
||||||
|
socket[t] = kw[t]
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self._ws, attr)
|
||||||
|
|
||||||
|
def __setattr__(self, attr, value):
|
||||||
|
if attr == "onmessage":
|
||||||
|
self._ws[attr] = lambda e: value(EventMessage(e))
|
||||||
|
else:
|
||||||
|
self._ws[attr] = value
|
||||||
|
|
||||||
|
def close(self, **kw):
|
||||||
|
if code in kw and reason in kw:
|
||||||
|
self._ws.close(kw[code], kw[reason])
|
||||||
|
elif code in kw:
|
||||||
|
self._ws.close(kw[code])
|
||||||
|
else:
|
||||||
|
self._ws.close()
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
if isinstance(data, str):
|
||||||
|
self._ws.send(data)
|
||||||
|
else:
|
||||||
|
buffer = js.Uint8Array.new(len(data))
|
||||||
|
for pos, b in enumerate(data):
|
||||||
|
buffer[pos] = b
|
||||||
|
self._ws.send(buffer)
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
from .pydom import JSProperty
|
||||||
from .pydom import dom as pydom
|
from .pydom import dom as pydom
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import inspect
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import Any
|
from typing import Any
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -34,6 +36,23 @@ from pyscript import display, document, window
|
|||||||
alert = window.alert
|
alert = window.alert
|
||||||
|
|
||||||
|
|
||||||
|
class JSProperty:
|
||||||
|
"""JS property descriptor that directly maps to the property with the same
|
||||||
|
name in the underlying JS component."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, allow_nones: bool = False):
|
||||||
|
self.name = name
|
||||||
|
self.allow_nones = allow_nones
|
||||||
|
|
||||||
|
def __get__(self, obj, objtype=None):
|
||||||
|
return getattr(obj._js, self.name)
|
||||||
|
|
||||||
|
def __set__(self, obj, value):
|
||||||
|
if not self.allow_nones and value is None:
|
||||||
|
return
|
||||||
|
setattr(obj._js, self.name, value)
|
||||||
|
|
||||||
|
|
||||||
class BaseElement:
|
class BaseElement:
|
||||||
def __init__(self, js_element):
|
def __init__(self, js_element):
|
||||||
self._js = js_element
|
self._js = js_element
|
||||||
@@ -104,7 +123,7 @@ class Element(BaseElement):
|
|||||||
# TODO: this is Pyodide specific for now!!!!!!
|
# TODO: this is Pyodide specific for now!!!!!!
|
||||||
# if we get passed a JSProxy Element directly we just map it to the
|
# if we get passed a JSProxy Element directly we just map it to the
|
||||||
# higher level Python element
|
# higher level Python element
|
||||||
if isinstance(child, JsProxy):
|
if inspect.isclass(JsProxy) and isinstance(child, JsProxy):
|
||||||
return self.append(Element(child))
|
return self.append(Element(child))
|
||||||
|
|
||||||
elif isinstance(child, Element):
|
elif isinstance(child, Element):
|
||||||
@@ -125,6 +144,14 @@ class Element(BaseElement):
|
|||||||
def html(self, value):
|
def html(self, value):
|
||||||
self._js.innerHTML = value
|
self._js.innerHTML = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
return self._js.textContent
|
||||||
|
|
||||||
|
@text.setter
|
||||||
|
def text(self, value):
|
||||||
|
self._js.textContent = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content(self):
|
def content(self):
|
||||||
# TODO: This breaks with with standard template elements. Define how to best
|
# TODO: This breaks with with standard template elements. Define how to best
|
||||||
@@ -259,7 +286,7 @@ class Element(BaseElement):
|
|||||||
canvas._js.width = width
|
canvas._js.width = width
|
||||||
canvas._js.height = height
|
canvas._js.height = height
|
||||||
|
|
||||||
elif isistance(to, Element):
|
elif isinstance(to, Element):
|
||||||
if to._js.tagName != "CANVAS":
|
if to._js.tagName != "CANVAS":
|
||||||
raise TypeError("Element to snap to must a canvas.")
|
raise TypeError("Element to snap to must a canvas.")
|
||||||
canvas = to
|
canvas = to
|
||||||
|
|||||||
1
pyscript.core/src/stdlib/pyweb/ui/__init__.py
Normal file
1
pyscript.core/src/stdlib/pyweb/ui/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import elements
|
||||||
947
pyscript.core/src/stdlib/pyweb/ui/elements.py
Normal file
947
pyscript.core/src/stdlib/pyweb/ui/elements.py
Normal file
@@ -0,0 +1,947 @@
|
|||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pyscript import document, when, window
|
||||||
|
from pyweb import JSProperty, pydom
|
||||||
|
|
||||||
|
#: A flag to show if MicroPython is the current Python interpreter.
|
||||||
|
is_micropython = "MicroPython" in sys.version
|
||||||
|
|
||||||
|
|
||||||
|
def getmembers_static(cls):
|
||||||
|
"""Cross-interpreter implementation of inspect.getmembers_static."""
|
||||||
|
|
||||||
|
if is_micropython: # pragma: no cover
|
||||||
|
return [(name, getattr(cls, name)) for name, _ in inspect.getmembers(cls)]
|
||||||
|
|
||||||
|
return inspect.getmembers_static(cls)
|
||||||
|
|
||||||
|
|
||||||
|
class ElementBase(pydom.Element):
|
||||||
|
tag = "div"
|
||||||
|
|
||||||
|
# GLOBAL ATTRIBUTES
|
||||||
|
# These are attribute that all elements have (this list is a subset of the official one)
|
||||||
|
# We are trying to capture the most used ones
|
||||||
|
accesskey = JSProperty("accesskey")
|
||||||
|
autofocus = JSProperty("autofocus")
|
||||||
|
autocapitalize = JSProperty("autocapitalize")
|
||||||
|
className = JSProperty("className")
|
||||||
|
contenteditable = JSProperty("contenteditable")
|
||||||
|
draggable = JSProperty("draggable")
|
||||||
|
enterkeyhint = JSProperty("enterkeyhint")
|
||||||
|
hidden = JSProperty("hidden")
|
||||||
|
id = JSProperty("id")
|
||||||
|
lang = JSProperty("lang")
|
||||||
|
nonce = JSProperty("nonce")
|
||||||
|
part = JSProperty("part")
|
||||||
|
popover = JSProperty("popover")
|
||||||
|
slot = JSProperty("slot")
|
||||||
|
spellcheck = JSProperty("spellcheck")
|
||||||
|
tabindex = JSProperty("tabindex")
|
||||||
|
title = JSProperty("title")
|
||||||
|
translate = JSProperty("translate")
|
||||||
|
virtualkeyboardpolicy = JSProperty("virtualkeyboardpolicy")
|
||||||
|
|
||||||
|
def __init__(self, style=None, **kwargs):
|
||||||
|
super().__init__(document.createElement(self.tag))
|
||||||
|
|
||||||
|
# set all the style properties provided in input
|
||||||
|
if isinstance(style, dict):
|
||||||
|
for key, value in style.items():
|
||||||
|
self.style[key] = value
|
||||||
|
elif style is None:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Style should be a dictionary, received {style} (type {type(style)}) instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
# IMPORTANT!!! This is used to auto-harvest all input arguments and set them as properties
|
||||||
|
self._init_properties(**kwargs)
|
||||||
|
|
||||||
|
def _init_properties(self, **kwargs):
|
||||||
|
"""Set all the properties (of type JSProperties) provided in input as properties
|
||||||
|
of the class instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: The properties to set
|
||||||
|
"""
|
||||||
|
# Look at all the properties of the class and see if they were provided in kwargs
|
||||||
|
for attr_name, attr in getmembers_static(self.__class__):
|
||||||
|
# For each one, actually check if it is a property of the class and set it
|
||||||
|
if isinstance(attr, JSProperty) and attr_name in kwargs:
|
||||||
|
try:
|
||||||
|
setattr(self, attr_name, kwargs[attr_name])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error setting {attr_name} to {kwargs[attr_name]}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class TextElementBase(ElementBase):
|
||||||
|
def __init__(self, content=None, style=None, **kwargs):
|
||||||
|
super().__init__(style=style, **kwargs)
|
||||||
|
|
||||||
|
# If it's an element, append the element
|
||||||
|
if isinstance(content, pydom.Element):
|
||||||
|
self.append(content)
|
||||||
|
# If it's a list of elements
|
||||||
|
elif isinstance(content, list):
|
||||||
|
for item in content:
|
||||||
|
self.append(item)
|
||||||
|
# If the content wasn't set just ignore
|
||||||
|
elif content is None:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Otherwise, set content as the html of the element
|
||||||
|
self.html = content
|
||||||
|
|
||||||
|
|
||||||
|
# IMPORTANT: For all HTML components defined below, we are not mapping all
|
||||||
|
# available attributes, just the global and the most common ones.
|
||||||
|
# If you need to access a specific attribute, you can always use the `_js.<attribute>`
|
||||||
|
class a(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a"""
|
||||||
|
|
||||||
|
tag = "a"
|
||||||
|
|
||||||
|
download = JSProperty("download")
|
||||||
|
href = JSProperty("href")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
rel = JSProperty("rel")
|
||||||
|
target = JSProperty("target")
|
||||||
|
type = JSProperty("type")
|
||||||
|
|
||||||
|
|
||||||
|
class abbr(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/abbr"""
|
||||||
|
|
||||||
|
tag = "abbr"
|
||||||
|
|
||||||
|
|
||||||
|
class address(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address"""
|
||||||
|
|
||||||
|
tag = "address"
|
||||||
|
|
||||||
|
|
||||||
|
class area(ElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area"""
|
||||||
|
|
||||||
|
tag = "area"
|
||||||
|
|
||||||
|
alt = JSProperty("alt")
|
||||||
|
coords = JSProperty("coords")
|
||||||
|
download = JSProperty("download")
|
||||||
|
href = JSProperty("href")
|
||||||
|
ping = JSProperty("ping")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
rel = JSProperty("rel")
|
||||||
|
shape = JSProperty("shape")
|
||||||
|
target = JSProperty("target")
|
||||||
|
|
||||||
|
|
||||||
|
class article(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article"""
|
||||||
|
|
||||||
|
tag = "article"
|
||||||
|
|
||||||
|
|
||||||
|
class aside(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/aside"""
|
||||||
|
|
||||||
|
tag = "aside"
|
||||||
|
|
||||||
|
|
||||||
|
class audio(ElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio"""
|
||||||
|
|
||||||
|
tag = "audio"
|
||||||
|
|
||||||
|
autoplay = JSProperty("autoplay")
|
||||||
|
controls = JSProperty("controls")
|
||||||
|
controlslist = JSProperty("controlslist")
|
||||||
|
crossorigin = JSProperty("crossorigin")
|
||||||
|
disableremoteplayback = JSProperty("disableremoteplayback")
|
||||||
|
loop = JSProperty("loop")
|
||||||
|
muted = JSProperty("muted")
|
||||||
|
preload = JSProperty("preload")
|
||||||
|
src = JSProperty("src")
|
||||||
|
|
||||||
|
|
||||||
|
class b(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b"""
|
||||||
|
|
||||||
|
tag = "b"
|
||||||
|
|
||||||
|
|
||||||
|
class blockquote(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote"""
|
||||||
|
|
||||||
|
tag = "blockquote"
|
||||||
|
|
||||||
|
cite = JSProperty("cite")
|
||||||
|
|
||||||
|
|
||||||
|
class br(ElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br"""
|
||||||
|
|
||||||
|
tag = "br"
|
||||||
|
|
||||||
|
|
||||||
|
class button(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button"""
|
||||||
|
|
||||||
|
tag = "button"
|
||||||
|
|
||||||
|
autofocus = JSProperty("autofocus")
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
form = JSProperty("form")
|
||||||
|
formaction = JSProperty("formaction")
|
||||||
|
formenctype = JSProperty("formenctype")
|
||||||
|
formmethod = JSProperty("formmethod")
|
||||||
|
formnovalidate = JSProperty("formnovalidate")
|
||||||
|
formtarget = JSProperty("formtarget")
|
||||||
|
name = JSProperty("name")
|
||||||
|
type = JSProperty("type")
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class canvas(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas"""
|
||||||
|
|
||||||
|
tag = "canvas"
|
||||||
|
|
||||||
|
height = JSProperty("height")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
class caption(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption"""
|
||||||
|
|
||||||
|
tag = "caption"
|
||||||
|
|
||||||
|
|
||||||
|
class cite(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/cite"""
|
||||||
|
|
||||||
|
tag = "cite"
|
||||||
|
|
||||||
|
|
||||||
|
class code(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code"""
|
||||||
|
|
||||||
|
tag = "code"
|
||||||
|
|
||||||
|
|
||||||
|
class data(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data"""
|
||||||
|
|
||||||
|
tag = "data"
|
||||||
|
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class datalist(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist"""
|
||||||
|
|
||||||
|
tag = "datalist"
|
||||||
|
|
||||||
|
|
||||||
|
class dd(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dd"""
|
||||||
|
|
||||||
|
tag = "dd"
|
||||||
|
|
||||||
|
|
||||||
|
class del_(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del"""
|
||||||
|
|
||||||
|
tag = "del"
|
||||||
|
|
||||||
|
cite = JSProperty("cite")
|
||||||
|
datetime = JSProperty("datetime")
|
||||||
|
|
||||||
|
|
||||||
|
class details(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details"""
|
||||||
|
|
||||||
|
tag = "details"
|
||||||
|
|
||||||
|
open = JSProperty("open")
|
||||||
|
|
||||||
|
|
||||||
|
class dialog(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog"""
|
||||||
|
|
||||||
|
tag = "dialog"
|
||||||
|
|
||||||
|
open = JSProperty("open")
|
||||||
|
|
||||||
|
|
||||||
|
class div(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div"""
|
||||||
|
|
||||||
|
tag = "div"
|
||||||
|
|
||||||
|
|
||||||
|
class dl(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl"""
|
||||||
|
|
||||||
|
tag = "dl"
|
||||||
|
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class dt(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt"""
|
||||||
|
|
||||||
|
tag = "dt"
|
||||||
|
|
||||||
|
|
||||||
|
class em(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em"""
|
||||||
|
|
||||||
|
tag = "em"
|
||||||
|
|
||||||
|
|
||||||
|
class embed(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed"""
|
||||||
|
|
||||||
|
tag = "embed"
|
||||||
|
|
||||||
|
height = JSProperty("height")
|
||||||
|
src = JSProperty("src")
|
||||||
|
type = JSProperty("type")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
class fieldset(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset"""
|
||||||
|
|
||||||
|
tag = "fieldset"
|
||||||
|
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
form = JSProperty("form")
|
||||||
|
name = JSProperty("name")
|
||||||
|
|
||||||
|
|
||||||
|
class figcaption(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption"""
|
||||||
|
|
||||||
|
tag = "figcaption"
|
||||||
|
|
||||||
|
|
||||||
|
class figure(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure"""
|
||||||
|
|
||||||
|
tag = "figure"
|
||||||
|
|
||||||
|
|
||||||
|
class footer(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/footer"""
|
||||||
|
|
||||||
|
tag = "footer"
|
||||||
|
|
||||||
|
|
||||||
|
class form(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form"""
|
||||||
|
|
||||||
|
tag = "form"
|
||||||
|
|
||||||
|
accept_charset = JSProperty("accept-charset")
|
||||||
|
action = JSProperty("action")
|
||||||
|
autocapitalize = JSProperty("autocapitalize")
|
||||||
|
autocomplete = JSProperty("autocomplete")
|
||||||
|
enctype = JSProperty("enctype")
|
||||||
|
name = JSProperty("name")
|
||||||
|
method = JSProperty("method")
|
||||||
|
nonvalidate = JSProperty("nonvalidate")
|
||||||
|
rel = JSProperty("rel")
|
||||||
|
target = JSProperty("target")
|
||||||
|
|
||||||
|
|
||||||
|
class h1(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1"""
|
||||||
|
|
||||||
|
tag = "h1"
|
||||||
|
|
||||||
|
|
||||||
|
class h2(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h2"""
|
||||||
|
|
||||||
|
tag = "h2"
|
||||||
|
|
||||||
|
|
||||||
|
class h3(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h3"""
|
||||||
|
|
||||||
|
tag = "h3"
|
||||||
|
|
||||||
|
|
||||||
|
class h4(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h4"""
|
||||||
|
|
||||||
|
tag = "h4"
|
||||||
|
|
||||||
|
|
||||||
|
class h5(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h5"""
|
||||||
|
|
||||||
|
tag = "h5"
|
||||||
|
|
||||||
|
|
||||||
|
class h6(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h6"""
|
||||||
|
|
||||||
|
tag = "h6"
|
||||||
|
|
||||||
|
|
||||||
|
class header(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header"""
|
||||||
|
|
||||||
|
tag = "header"
|
||||||
|
|
||||||
|
|
||||||
|
class hgroup(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup"""
|
||||||
|
|
||||||
|
tag = "hgroup"
|
||||||
|
|
||||||
|
|
||||||
|
class hr(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr"""
|
||||||
|
|
||||||
|
tag = "hr"
|
||||||
|
|
||||||
|
|
||||||
|
class i(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i"""
|
||||||
|
|
||||||
|
tag = "i"
|
||||||
|
|
||||||
|
|
||||||
|
class iframe(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe"""
|
||||||
|
|
||||||
|
tag = "iframe"
|
||||||
|
|
||||||
|
allow = JSProperty("allow")
|
||||||
|
allowfullscreen = JSProperty("allowfullscreen")
|
||||||
|
height = JSProperty("height")
|
||||||
|
loading = JSProperty("loading")
|
||||||
|
name = JSProperty("name")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
sandbox = JSProperty("sandbox")
|
||||||
|
src = JSProperty("src")
|
||||||
|
srcdoc = JSProperty("srcdoc")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
class img(ElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img"""
|
||||||
|
|
||||||
|
tag = "img"
|
||||||
|
|
||||||
|
alt = JSProperty("alt")
|
||||||
|
crossorigin = JSProperty("crossorigin")
|
||||||
|
decoding = JSProperty("decoding")
|
||||||
|
fetchpriority = JSProperty("fetchpriority")
|
||||||
|
height = JSProperty("height")
|
||||||
|
ismap = JSProperty("ismap")
|
||||||
|
loading = JSProperty("loading")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
sizes = JSProperty("sizes")
|
||||||
|
src = JSProperty("src")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: Input is a reserved keyword in Python, so we use input_ instead
|
||||||
|
class input_(ElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input"""
|
||||||
|
|
||||||
|
tag = "input"
|
||||||
|
|
||||||
|
accept = JSProperty("accept")
|
||||||
|
alt = JSProperty("alt")
|
||||||
|
autofocus = JSProperty("autofocus")
|
||||||
|
capture = JSProperty("capture")
|
||||||
|
checked = JSProperty("checked")
|
||||||
|
dirname = JSProperty("dirname")
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
form = JSProperty("form")
|
||||||
|
formaction = JSProperty("formaction")
|
||||||
|
formenctype = JSProperty("formenctype")
|
||||||
|
formmethod = JSProperty("formmethod")
|
||||||
|
formnovalidate = JSProperty("formnovalidate")
|
||||||
|
formtarget = JSProperty("formtarget")
|
||||||
|
height = JSProperty("height")
|
||||||
|
list = JSProperty("list")
|
||||||
|
max = JSProperty("max")
|
||||||
|
maxlength = JSProperty("maxlength")
|
||||||
|
min = JSProperty("min")
|
||||||
|
minlength = JSProperty("minlength")
|
||||||
|
multiple = JSProperty("multiple")
|
||||||
|
name = JSProperty("name")
|
||||||
|
pattern = JSProperty("pattern")
|
||||||
|
placeholder = JSProperty("placeholder")
|
||||||
|
popovertarget = JSProperty("popovertarget")
|
||||||
|
popovertargetaction = JSProperty("popovertargetaction")
|
||||||
|
readonly = JSProperty("readonly")
|
||||||
|
required = JSProperty("required")
|
||||||
|
size = JSProperty("size")
|
||||||
|
src = JSProperty("src")
|
||||||
|
step = JSProperty("step")
|
||||||
|
type = JSProperty("type")
|
||||||
|
value = JSProperty("value")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
class ins(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins"""
|
||||||
|
|
||||||
|
tag = "ins"
|
||||||
|
|
||||||
|
cite = JSProperty("cite")
|
||||||
|
datetime = JSProperty("datetime")
|
||||||
|
|
||||||
|
|
||||||
|
class kbd(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd"""
|
||||||
|
|
||||||
|
tag = "kbd"
|
||||||
|
|
||||||
|
|
||||||
|
class label(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label"""
|
||||||
|
|
||||||
|
tag = "label"
|
||||||
|
|
||||||
|
for_ = JSProperty("for")
|
||||||
|
|
||||||
|
|
||||||
|
class legend(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend"""
|
||||||
|
|
||||||
|
tag = "legend"
|
||||||
|
|
||||||
|
|
||||||
|
class li(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li"""
|
||||||
|
|
||||||
|
tag = "li"
|
||||||
|
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class link(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link"""
|
||||||
|
|
||||||
|
tag = "link"
|
||||||
|
|
||||||
|
as_ = JSProperty("as")
|
||||||
|
crossorigin = JSProperty("crossorigin")
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
fetchpriority = JSProperty("fetchpriority")
|
||||||
|
href = JSProperty("href")
|
||||||
|
imagesizes = JSProperty("imagesizes")
|
||||||
|
imagesrcset = JSProperty("imagesrcset")
|
||||||
|
integrity = JSProperty("integrity")
|
||||||
|
media = JSProperty("media")
|
||||||
|
rel = JSProperty("rel")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
sizes = JSProperty("sizes")
|
||||||
|
title = JSProperty("title")
|
||||||
|
type = JSProperty("type")
|
||||||
|
|
||||||
|
|
||||||
|
class main(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/main"""
|
||||||
|
|
||||||
|
tag = "main"
|
||||||
|
|
||||||
|
|
||||||
|
class map_(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map"""
|
||||||
|
|
||||||
|
tag = "map"
|
||||||
|
|
||||||
|
name = JSProperty("name")
|
||||||
|
|
||||||
|
|
||||||
|
class mark(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark"""
|
||||||
|
|
||||||
|
tag = "mark"
|
||||||
|
|
||||||
|
|
||||||
|
class menu(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu"""
|
||||||
|
|
||||||
|
tag = "menu"
|
||||||
|
|
||||||
|
|
||||||
|
class meter(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter"""
|
||||||
|
|
||||||
|
tag = "meter"
|
||||||
|
|
||||||
|
form = JSProperty("form")
|
||||||
|
high = JSProperty("high")
|
||||||
|
low = JSProperty("low")
|
||||||
|
max = JSProperty("max")
|
||||||
|
min = JSProperty("min")
|
||||||
|
optimum = JSProperty("optimum")
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class nav(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav"""
|
||||||
|
|
||||||
|
tag = "nav"
|
||||||
|
|
||||||
|
|
||||||
|
class object_(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object"""
|
||||||
|
|
||||||
|
tag = "object"
|
||||||
|
|
||||||
|
data = JSProperty("data")
|
||||||
|
form = JSProperty("form")
|
||||||
|
height = JSProperty("height")
|
||||||
|
name = JSProperty("name")
|
||||||
|
type = JSProperty("type")
|
||||||
|
usemap = JSProperty("usemap")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
class ol(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol"""
|
||||||
|
|
||||||
|
tag = "ol"
|
||||||
|
|
||||||
|
reversed = JSProperty("reversed")
|
||||||
|
start = JSProperty("start")
|
||||||
|
type = JSProperty("type")
|
||||||
|
|
||||||
|
|
||||||
|
class optgroup(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup"""
|
||||||
|
|
||||||
|
tag = "optgroup"
|
||||||
|
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
label = JSProperty("label")
|
||||||
|
|
||||||
|
|
||||||
|
class option(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option"""
|
||||||
|
|
||||||
|
tag = "option"
|
||||||
|
|
||||||
|
disabled = JSProperty("value")
|
||||||
|
label = JSProperty("label")
|
||||||
|
selected = JSProperty("selected")
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class output(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output"""
|
||||||
|
|
||||||
|
tag = "output"
|
||||||
|
|
||||||
|
for_ = JSProperty("for")
|
||||||
|
form = JSProperty("form")
|
||||||
|
name = JSProperty("name")
|
||||||
|
|
||||||
|
|
||||||
|
class p(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p"""
|
||||||
|
|
||||||
|
tag = "p"
|
||||||
|
|
||||||
|
|
||||||
|
class picture(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture"""
|
||||||
|
|
||||||
|
tag = "picture"
|
||||||
|
|
||||||
|
|
||||||
|
class pre(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre"""
|
||||||
|
|
||||||
|
tag = "pre"
|
||||||
|
|
||||||
|
|
||||||
|
class progress(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress"""
|
||||||
|
|
||||||
|
tag = "progress"
|
||||||
|
|
||||||
|
max = JSProperty("max")
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class q(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q"""
|
||||||
|
|
||||||
|
tag = "q"
|
||||||
|
|
||||||
|
cite = JSProperty("cite")
|
||||||
|
|
||||||
|
|
||||||
|
class s(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/s"""
|
||||||
|
|
||||||
|
tag = "s"
|
||||||
|
|
||||||
|
|
||||||
|
class script(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script"""
|
||||||
|
|
||||||
|
tag = "script"
|
||||||
|
|
||||||
|
# Let's add async manually since it's a reserved keyword in Python
|
||||||
|
async_ = JSProperty("async")
|
||||||
|
blocking = JSProperty("blocking")
|
||||||
|
crossorigin = JSProperty("crossorigin")
|
||||||
|
defer = JSProperty("defer")
|
||||||
|
fetchpriority = JSProperty("fetchpriority")
|
||||||
|
integrity = JSProperty("integrity")
|
||||||
|
nomodule = JSProperty("nomodule")
|
||||||
|
nonce = JSProperty("nonce")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
src = JSProperty("src")
|
||||||
|
type = JSProperty("type")
|
||||||
|
|
||||||
|
|
||||||
|
class section(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section"""
|
||||||
|
|
||||||
|
tag = "section"
|
||||||
|
|
||||||
|
|
||||||
|
class select(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select"""
|
||||||
|
|
||||||
|
tag = "select"
|
||||||
|
|
||||||
|
|
||||||
|
class small(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/small"""
|
||||||
|
|
||||||
|
tag = "small"
|
||||||
|
|
||||||
|
|
||||||
|
class source(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source"""
|
||||||
|
|
||||||
|
tag = "source"
|
||||||
|
|
||||||
|
media = JSProperty("media")
|
||||||
|
sizes = JSProperty("sizes")
|
||||||
|
src = JSProperty("src")
|
||||||
|
srcset = JSProperty("srcset")
|
||||||
|
type = JSProperty("type")
|
||||||
|
|
||||||
|
|
||||||
|
class span(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span"""
|
||||||
|
|
||||||
|
tag = "span"
|
||||||
|
|
||||||
|
|
||||||
|
class strong(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong"""
|
||||||
|
|
||||||
|
tag = "strong"
|
||||||
|
|
||||||
|
|
||||||
|
class style(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style"""
|
||||||
|
|
||||||
|
tag = "style"
|
||||||
|
|
||||||
|
blocking = JSProperty("blocking")
|
||||||
|
media = JSProperty("media")
|
||||||
|
nonce = JSProperty("nonce")
|
||||||
|
title = JSProperty("title")
|
||||||
|
|
||||||
|
|
||||||
|
class sub(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub"""
|
||||||
|
|
||||||
|
tag = "sub"
|
||||||
|
|
||||||
|
|
||||||
|
class summary(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary"""
|
||||||
|
|
||||||
|
tag = "summary"
|
||||||
|
|
||||||
|
|
||||||
|
class sup(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup"""
|
||||||
|
|
||||||
|
tag = "sup"
|
||||||
|
|
||||||
|
|
||||||
|
class table(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table"""
|
||||||
|
|
||||||
|
tag = "table"
|
||||||
|
|
||||||
|
|
||||||
|
class tbody(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody"""
|
||||||
|
|
||||||
|
tag = "tbody"
|
||||||
|
|
||||||
|
|
||||||
|
class td(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td"""
|
||||||
|
|
||||||
|
tag = "td"
|
||||||
|
|
||||||
|
colspan = JSProperty("colspan")
|
||||||
|
headers = JSProperty("headers")
|
||||||
|
rowspan = JSProperty("rowspan")
|
||||||
|
|
||||||
|
|
||||||
|
class template(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template"""
|
||||||
|
|
||||||
|
tag = "template"
|
||||||
|
|
||||||
|
shadowrootmode = JSProperty("shadowrootmode")
|
||||||
|
|
||||||
|
|
||||||
|
class textarea(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea"""
|
||||||
|
|
||||||
|
tag = "textarea"
|
||||||
|
|
||||||
|
autocapitalize = JSProperty("autocapitalize")
|
||||||
|
autocomplete = JSProperty("autocomplete")
|
||||||
|
autofocus = JSProperty("autofocus")
|
||||||
|
cols = JSProperty("cols")
|
||||||
|
dirname = JSProperty("dirname")
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
form = JSProperty("form")
|
||||||
|
maxlength = JSProperty("maxlength")
|
||||||
|
minlength = JSProperty("minlength")
|
||||||
|
name = JSProperty("name")
|
||||||
|
placeholder = JSProperty("placeholder")
|
||||||
|
readonly = JSProperty("readonly")
|
||||||
|
required = JSProperty("required")
|
||||||
|
rows = JSProperty("rows")
|
||||||
|
spellcheck = JSProperty("spellcheck")
|
||||||
|
wrap = JSProperty("wrap")
|
||||||
|
|
||||||
|
|
||||||
|
class tfoot(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tfoot"""
|
||||||
|
|
||||||
|
tag = "tfoot"
|
||||||
|
|
||||||
|
|
||||||
|
class th(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th"""
|
||||||
|
|
||||||
|
tag = "th"
|
||||||
|
|
||||||
|
|
||||||
|
class thead(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead"""
|
||||||
|
|
||||||
|
tag = "thead"
|
||||||
|
|
||||||
|
|
||||||
|
class time(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time"""
|
||||||
|
|
||||||
|
tag = "time"
|
||||||
|
|
||||||
|
datetime = JSProperty("datetime")
|
||||||
|
|
||||||
|
|
||||||
|
class title(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title"""
|
||||||
|
|
||||||
|
tag = "title"
|
||||||
|
|
||||||
|
|
||||||
|
class tr(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr"""
|
||||||
|
|
||||||
|
tag = "tr"
|
||||||
|
|
||||||
|
abbr = JSProperty("abbr")
|
||||||
|
colspan = JSProperty("colspan")
|
||||||
|
headers = JSProperty("headers")
|
||||||
|
rowspan = JSProperty("rowspan")
|
||||||
|
scope = JSProperty("scope")
|
||||||
|
|
||||||
|
|
||||||
|
class track(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track"""
|
||||||
|
|
||||||
|
tag = "track"
|
||||||
|
|
||||||
|
default = JSProperty("default")
|
||||||
|
kind = JSProperty("kind")
|
||||||
|
label = JSProperty("label")
|
||||||
|
src = JSProperty("src")
|
||||||
|
srclang = JSProperty("srclang")
|
||||||
|
|
||||||
|
|
||||||
|
class u(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/u"""
|
||||||
|
|
||||||
|
tag = "u"
|
||||||
|
|
||||||
|
|
||||||
|
class ul(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul"""
|
||||||
|
|
||||||
|
tag = "ul"
|
||||||
|
|
||||||
|
|
||||||
|
class var(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/var"""
|
||||||
|
|
||||||
|
tag = "var"
|
||||||
|
|
||||||
|
|
||||||
|
class video(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video"""
|
||||||
|
|
||||||
|
tag = "video"
|
||||||
|
|
||||||
|
autoplay = JSProperty("autoplay")
|
||||||
|
controls = JSProperty("controls")
|
||||||
|
crossorigin = JSProperty("crossorigin")
|
||||||
|
disablepictureinpicture = JSProperty("disablepictureinpicture")
|
||||||
|
disableremoteplayback = JSProperty("disableremoteplayback")
|
||||||
|
height = JSProperty("height")
|
||||||
|
loop = JSProperty("loop")
|
||||||
|
muted = JSProperty("muted")
|
||||||
|
playsinline = JSProperty("playsinline")
|
||||||
|
poster = JSProperty("poster")
|
||||||
|
preload = JSProperty("preload")
|
||||||
|
src = JSProperty("src")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
# Custom Elements
|
||||||
|
class grid(TextElementBase):
|
||||||
|
tag = "div"
|
||||||
|
|
||||||
|
def __init__(self, layout, content=None, gap=None, **kwargs):
|
||||||
|
super().__init__(content, **kwargs)
|
||||||
|
self.style["display"] = "grid"
|
||||||
|
self.style["grid-template-columns"] = layout
|
||||||
|
|
||||||
|
# TODO: This should be a property
|
||||||
|
if not gap is None:
|
||||||
|
self.style["gap"] = gap
|
||||||
19
pyscript.core/test/code-a-part.html
Normal file
19
pyscript.core/test/code-a-part.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module">
|
||||||
|
import { hooks } from "../dist/core.js";
|
||||||
|
hooks.main.codeBeforeRun.add('print(0)');
|
||||||
|
hooks.main.codeAfterRun.add('print(2)');
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py">
|
||||||
|
# raise an error instead to see it on line 1
|
||||||
|
print(1)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -8,9 +8,17 @@
|
|||||||
<script type="module" src="../dist/core.js"></script>
|
<script type="module" src="../dist/core.js"></script>
|
||||||
<mpy-config src="config-url/config.json"></mpy-config>
|
<mpy-config src="config-url/config.json"></mpy-config>
|
||||||
<script type="mpy">
|
<script type="mpy">
|
||||||
|
from pyscript import config
|
||||||
|
if config["files"]["{TO}"] != "./runtime":
|
||||||
|
raise Exception("wrong config tree")
|
||||||
|
|
||||||
from runtime import test
|
from runtime import test
|
||||||
</script>
|
</script>
|
||||||
<script type="mpy" worker>
|
<script type="mpy" worker>
|
||||||
|
from pyscript import config
|
||||||
|
if config["files"]["{TO}"] != "./runtime":
|
||||||
|
raise Exception("wrong config tree")
|
||||||
|
|
||||||
from runtime import test
|
from runtime import test
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
95
pyscript.core/test/fetch.html
Normal file
95
pyscript.core/test/fetch.html
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module">
|
||||||
|
import fetch from 'https://esm.run/@webreflection/fetch';
|
||||||
|
|
||||||
|
globalThis.fetch_text = await fetch("config.json").text();
|
||||||
|
globalThis.fetch_json = JSON.stringify(await fetch("config.json").json());
|
||||||
|
globalThis.fetch_buffer = new Uint8Array((await fetch("config.json").arrayBuffer())).length;
|
||||||
|
|
||||||
|
document.head.appendChild(
|
||||||
|
Object.assign(
|
||||||
|
document.createElement('script'),
|
||||||
|
{
|
||||||
|
type: 'module',
|
||||||
|
src: '../dist/core.js'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<script type="mpy" async>
|
||||||
|
import js, json
|
||||||
|
from pyscript import document, fetch
|
||||||
|
|
||||||
|
fetch_text = await (await fetch("config.json")).text()
|
||||||
|
if (fetch_text != js.fetch_text):
|
||||||
|
raise Exception("fetch_text")
|
||||||
|
|
||||||
|
fetch_text = await fetch("config.json").text()
|
||||||
|
if (fetch_text != js.fetch_text):
|
||||||
|
raise Exception("fetch_text")
|
||||||
|
|
||||||
|
fetch_json = await (await fetch("config.json")).json()
|
||||||
|
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||||
|
raise Exception("fetch_json")
|
||||||
|
|
||||||
|
fetch_json = await fetch("config.json").json()
|
||||||
|
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||||
|
raise Exception("fetch_json")
|
||||||
|
|
||||||
|
fetch_buffer = await (await fetch("config.json")).arrayBuffer()
|
||||||
|
if (len(fetch_buffer) != js.fetch_buffer):
|
||||||
|
raise Exception("fetch_buffer")
|
||||||
|
|
||||||
|
fetch_buffer = await fetch("config.json").arrayBuffer()
|
||||||
|
if (len(fetch_buffer) != js.fetch_buffer):
|
||||||
|
raise Exception("fetch_buffer")
|
||||||
|
|
||||||
|
print(await (await fetch("config.json")).bytearray())
|
||||||
|
print(await (await fetch("config.json")).blob())
|
||||||
|
|
||||||
|
if (await fetch("shenanigans.nope")).ok == False:
|
||||||
|
document.documentElement.classList.add('mpy')
|
||||||
|
</script>
|
||||||
|
<script type="py" async>
|
||||||
|
import js, json
|
||||||
|
from pyscript import document, fetch
|
||||||
|
|
||||||
|
fetch_text = await (await fetch("config.json")).text()
|
||||||
|
if (fetch_text != js.fetch_text):
|
||||||
|
raise Exception("fetch_text")
|
||||||
|
|
||||||
|
fetch_text = await fetch("config.json").text()
|
||||||
|
if (fetch_text != js.fetch_text):
|
||||||
|
raise Exception("fetch_text")
|
||||||
|
|
||||||
|
fetch_json = await (await fetch("config.json")).json()
|
||||||
|
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||||
|
raise Exception("fetch_json")
|
||||||
|
|
||||||
|
fetch_json = await fetch("config.json").json()
|
||||||
|
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||||
|
raise Exception("fetch_json")
|
||||||
|
|
||||||
|
fetch_buffer = await (await fetch("config.json")).arrayBuffer()
|
||||||
|
if (len(fetch_buffer) != js.fetch_buffer):
|
||||||
|
raise Exception("fetch_buffer")
|
||||||
|
|
||||||
|
fetch_buffer = await fetch("config.json").arrayBuffer()
|
||||||
|
if (len(fetch_buffer) != js.fetch_buffer):
|
||||||
|
raise Exception("fetch_buffer")
|
||||||
|
|
||||||
|
print(await (await fetch("config.json")).bytearray())
|
||||||
|
print(await (await fetch("config.json")).blob())
|
||||||
|
|
||||||
|
if (await fetch("shenanigans.nope")).ok == False:
|
||||||
|
document.documentElement.classList.add('py')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
pyscript.core/test/ffi.html
Normal file
26
pyscript.core/test/ffi.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>PyScript FFI</title>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="mpy">
|
||||||
|
from pyscript import document
|
||||||
|
from pyscript.ffi import to_js
|
||||||
|
document.documentElement.classList.add(
|
||||||
|
to_js({"ok": "mpy"}).ok
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
<script type="py">
|
||||||
|
from pyscript import document
|
||||||
|
from pyscript.ffi import to_js
|
||||||
|
document.documentElement.classList.add(
|
||||||
|
to_js({"ok": "py"}).ok
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -78,3 +78,13 @@ test('Pyodide + multiple terminals via Worker', async ({ page }) => {
|
|||||||
await page.goto('http://localhost:8080/test/py-terminals.html');
|
await page.goto('http://localhost:8080/test/py-terminals.html');
|
||||||
await page.waitForSelector('html.first.second');
|
await page.waitForSelector('html.first.second');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('MicroPython + Pyodide fetch', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080/test/fetch.html');
|
||||||
|
await page.waitForSelector('html.mpy.py');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MicroPython + Pyodide ffi', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080/test/ffi.html');
|
||||||
|
await page.waitForSelector('html.mpy.py');
|
||||||
|
});
|
||||||
|
|||||||
23
pyscript.core/test/no_sab/index.html
Normal file
23
pyscript.core/test/no_sab/index.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module">
|
||||||
|
import { PyWorker } from '../../dist/core.js';
|
||||||
|
const { sync } = await PyWorker(
|
||||||
|
'./worker.py',
|
||||||
|
{
|
||||||
|
config: {
|
||||||
|
sync_main_only: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
document.documentElement.classList.add(
|
||||||
|
await sync.get_class()
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
3
pyscript.core/test/no_sab/worker.py
Normal file
3
pyscript.core/test/no_sab/worker.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from pyscript import sync
|
||||||
|
|
||||||
|
sync.get_class = lambda: "ok"
|
||||||
2
pyscript.core/test/py-editor/config.toml
Normal file
2
pyscript.core/test/py-editor/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[js_modules.worker]
|
||||||
|
"https://cdn.jsdelivr.net/npm/html-escaper/+esm" = "html_escaper"
|
||||||
55
pyscript.core/test/py-editor/index.html
Normal file
55
pyscript.core/test/py-editor/index.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module">
|
||||||
|
import '../../dist/core.js';
|
||||||
|
|
||||||
|
addEventListener('mpy-editor', async ({ target }) => {
|
||||||
|
if (target.hasAttribute('setup')) {
|
||||||
|
await target.process([
|
||||||
|
'from pyscript import document',
|
||||||
|
// adds class="a-1" to the <html> element
|
||||||
|
'document.documentElement.classList.add(f"a-{a}")',
|
||||||
|
'from js import console',
|
||||||
|
'console.log("Hello JS")',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- a setup node with a config for an env -->
|
||||||
|
<script type="mpy-editor" src="task1.py" config="./config.toml" env="task1" setup></script>
|
||||||
|
<script type="mpy-editor" env="task1">
|
||||||
|
from pyscript.js_modules.html_escaper import escape, unescape
|
||||||
|
print(unescape(escape("<OK>")))
|
||||||
|
a = 1
|
||||||
|
</script>
|
||||||
|
<!-- a share-nothing micropython editor -->
|
||||||
|
<script type="mpy-editor" config="./config.toml">
|
||||||
|
from pyscript.js_modules.html_escaper import escape, unescape
|
||||||
|
print(unescape(escape("<OK>")))
|
||||||
|
b = 2
|
||||||
|
try:
|
||||||
|
print(a)
|
||||||
|
except:
|
||||||
|
print("all good")
|
||||||
|
</script>
|
||||||
|
<!-- a config once micropython env -->
|
||||||
|
<script type="mpy-editor" env="task2" config="./config.toml">
|
||||||
|
from pyscript.js_modules.html_escaper import escape, unescape
|
||||||
|
print(unescape(escape("<OK>")))
|
||||||
|
c = 3
|
||||||
|
try:
|
||||||
|
print(b)
|
||||||
|
except:
|
||||||
|
print("all good")
|
||||||
|
</script>
|
||||||
|
<script type="mpy-editor" env="task2">
|
||||||
|
print(c)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
pyscript.core/test/py-editor/issue-2056.html
Normal file
17
pyscript.core/test/py-editor/issue-2056.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py-editor">
|
||||||
|
print("Hello!")
|
||||||
|
</script>
|
||||||
|
<script type="mpy-editor">
|
||||||
|
print("Hello!")
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
pyscript.core/test/py-editor/task1.py
Normal file
5
pyscript.core/test/py-editor/task1.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from pyscript import window
|
||||||
|
|
||||||
|
window.console.log("OK")
|
||||||
|
|
||||||
|
a = 1
|
||||||
@@ -10,6 +10,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="py" src="terminal.py" worker terminal></script>
|
<script type="py" src="terminal.py" worker terminal></script>
|
||||||
<script type="py" src="terminal.py" worker terminal></script>
|
<script type="mpy" src="terminal.py" worker terminal></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,24 +9,10 @@
|
|||||||
<style>.xterm { padding: .5rem; }</style>
|
<style>.xterm { padding: .5rem; }</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="py">
|
<script type="mpy" worker terminal>
|
||||||
def greetings(event):
|
print("µpython")
|
||||||
print('hello world')
|
import code
|
||||||
|
code.interact()
|
||||||
</script>
|
</script>
|
||||||
<py-script worker terminal>
|
|
||||||
# works on both worker and main scripts
|
|
||||||
print("__terminal__", __terminal__)
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pyscript import display, document
|
|
||||||
display("Hello", "PyScript Next - PyTerminal", append=False)
|
|
||||||
print("this should go to the terminal")
|
|
||||||
print("another line")
|
|
||||||
|
|
||||||
# this works as expected
|
|
||||||
print("this goes to stderr", file=sys.stderr)
|
|
||||||
document.addEventListener('click', lambda event: print(event.type));
|
|
||||||
</py-script>
|
|
||||||
<button id="my-button" py-click="greetings">Click me</button>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
27
pyscript.core/test/py_modules.html
Normal file
27
pyscript.core/test/py_modules.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py" async>
|
||||||
|
from pyscript import py_import, js_import, window
|
||||||
|
|
||||||
|
window.console.time("first")
|
||||||
|
matplotlib, regex, = await py_import("matplotlib", "regex")
|
||||||
|
window.console.timeEnd("first")
|
||||||
|
|
||||||
|
window.console.time("second")
|
||||||
|
matplotlib, regex, = await py_import("matplotlib", "regex")
|
||||||
|
window.console.timeEnd("second")
|
||||||
|
|
||||||
|
print(matplotlib, regex)
|
||||||
|
|
||||||
|
escaper, = await js_import("https://esm.run/html-escaper")
|
||||||
|
window.console.log(escaper)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<script type="module" src="../dist/core.js"></script>
|
<script type="module" src="../dist/core.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="py" src="pydom.py"></script>
|
<script type="mpy" src="pydom.py"></script>
|
||||||
|
|
||||||
<button id="just-a-button">Click For Time</button>
|
<button id="just-a-button">Click For Time</button>
|
||||||
<button id="color-button">Click For Color</button>
|
<button id="color-button">Click For Color</button>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import random
|
import random
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime as dt
|
from datetime import datetime as dt
|
||||||
|
|
||||||
from pyscript import display, when
|
from pyscript import display, when
|
||||||
from pyweb import pydom
|
from pyweb import pydom
|
||||||
|
|
||||||
|
display(sys.version, target="system-info")
|
||||||
|
|
||||||
|
|
||||||
@when("click", "#just-a-button")
|
@when("click", "#just-a-button")
|
||||||
def on_click():
|
def on_click():
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<script type="mpy" src="pydom.py"></script>
|
<script type="mpy" src="pydom.py"></script>
|
||||||
|
|
||||||
|
<div id="system-info"></div>
|
||||||
|
|
||||||
<button id="just-a-button">Click For Time</button>
|
<button id="just-a-button">Click For Time</button>
|
||||||
<button id="color-button">Click For Color</button>
|
<button id="color-button">Click For Color</button>
|
||||||
<button id="color-reset-button">Reset Color</button>
|
<button id="color-reset-button">Reset Color</button>
|
||||||
|
|||||||
@@ -98,6 +98,8 @@
|
|||||||
<p class="collection"></p>
|
<p class="collection"></p>
|
||||||
<div class="collection"></div>
|
<div class="collection"></div>
|
||||||
<h3 class="collection"></h3>
|
<h3 class="collection"></h3>
|
||||||
|
|
||||||
|
<div id="element_attribute_tests"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,30 @@ class TestElement:
|
|||||||
|
|
||||||
assert called
|
assert called
|
||||||
|
|
||||||
|
def test_html_attribute(self):
|
||||||
|
# GIVEN an existing element on the page with a known empty text content
|
||||||
|
div = pydom["#element_attribute_tests"][0]
|
||||||
|
|
||||||
|
# WHEN we set the html attribute
|
||||||
|
div.html = "<b>New Content</b>"
|
||||||
|
|
||||||
|
# EXPECT the element html and underlying JS Element innerHTML property
|
||||||
|
# to match what we expect and what
|
||||||
|
assert div.html == div._js.innerHTML == "<b>New Content</b>"
|
||||||
|
assert div.text == div._js.textContent == "New Content"
|
||||||
|
|
||||||
|
def test_text_attribute(self):
|
||||||
|
# GIVEN an existing element on the page with a known empty text content
|
||||||
|
div = pydom["#element_attribute_tests"][0]
|
||||||
|
|
||||||
|
# WHEN we set the html attribute
|
||||||
|
div.text = "<b>New Content</b>"
|
||||||
|
|
||||||
|
# EXPECT the element html and underlying JS Element innerHTML property
|
||||||
|
# to match what we expect and what
|
||||||
|
assert div.html == div._js.innerHTML == "<b>New Content</b>"
|
||||||
|
assert div.text == div._js.textContent == "<b>New Content</b>"
|
||||||
|
|
||||||
|
|
||||||
class TestCollection:
|
class TestCollection:
|
||||||
def test_iter_eq_children(self):
|
def test_iter_eq_children(self):
|
||||||
|
|||||||
0
pyscript.core/test/test.html
Normal file
0
pyscript.core/test/test.html
Normal file
251
pyscript.core/test/ui/demo.py
Normal file
251
pyscript.core/test/ui/demo.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
try:
|
||||||
|
from textwrap import dedent
|
||||||
|
except ImportError:
|
||||||
|
dedent = lambda x: x
|
||||||
|
|
||||||
|
import examples
|
||||||
|
import shoelace
|
||||||
|
import styles
|
||||||
|
from markdown import markdown
|
||||||
|
from pyscript import when, window
|
||||||
|
from pyweb import pydom
|
||||||
|
from pyweb.ui import elements as el
|
||||||
|
from pyweb.ui.elements import a, button, div, grid, h1, h2, h3
|
||||||
|
|
||||||
|
MAIN_PAGE_MARKDOWN = dedent(
|
||||||
|
"""
|
||||||
|
## What is pyweb.ui?
|
||||||
|
Pyweb UI is a totally immagnary exercise atm but..... imagine it is a Python library that allows you to create
|
||||||
|
web applications using Python only.
|
||||||
|
|
||||||
|
It is based on base HTML/JS components but is extensible, for instance, it can have a [Shoelace](https://shoelace.style/) backend...
|
||||||
|
|
||||||
|
PyWeb is a Python library that allows you to create web applications using Python only.
|
||||||
|
|
||||||
|
## What can I do with Pyweb.ui?
|
||||||
|
|
||||||
|
You can create web applications using Python only.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# First thing we do is to load all the external resources we need
|
||||||
|
shoelace.load_resources()
|
||||||
|
|
||||||
|
|
||||||
|
# Let's define some convenience functions first
|
||||||
|
def create_component_details(component_label, component):
|
||||||
|
"""Create a component details card.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component (str): The name of the component to create.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the component created
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get the example from the examples catalog
|
||||||
|
example = component["instance"]
|
||||||
|
details = (
|
||||||
|
getattr(example, "__doc__", "")
|
||||||
|
or f"Details missing for component {component_label}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return div(
|
||||||
|
[
|
||||||
|
# Title and description (description is picked from the class docstring)
|
||||||
|
h1(component_label),
|
||||||
|
markdown(details),
|
||||||
|
# Example section
|
||||||
|
h2("Example:"),
|
||||||
|
create_component_example(component["instance"], component["code"]),
|
||||||
|
],
|
||||||
|
style={"margin": "20px"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_component_section(component_label, component, parent_div):
|
||||||
|
"""Create a link to a component and add it to the left panel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component (str): The name of the component to add.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the component created
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Create the component link element
|
||||||
|
div_ = div(
|
||||||
|
a(component_label, href="#"),
|
||||||
|
style={"display": "block", "text-align": "center", "margin": "auto"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a handler that opens the component details when the link is clicked
|
||||||
|
@when("click", div_)
|
||||||
|
def _change():
|
||||||
|
new_main = create_component_details(component_label, component)
|
||||||
|
main_area.html = ""
|
||||||
|
main_area.append(new_main)
|
||||||
|
|
||||||
|
# Add the new link element to the parent div (left panel)
|
||||||
|
parent_div.append(div_)
|
||||||
|
return div_
|
||||||
|
|
||||||
|
|
||||||
|
def create_component_example(widget, code):
|
||||||
|
"""Create a grid div with the widget on the left side and the relate code
|
||||||
|
on the right side.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget (ElementBase): The widget to add to the grid.
|
||||||
|
code (str): The code to add to the grid.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the grid created
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Create the grid that splits the window in two columns (25% and 75%)
|
||||||
|
grid_ = grid("29% 2% 74%")
|
||||||
|
|
||||||
|
# Add the widget
|
||||||
|
grid_.append(div(widget, style=styles.STYLE_EXAMPLE_INSTANCE))
|
||||||
|
|
||||||
|
# Add the code div
|
||||||
|
widget_code = markdown(dedent(f"""```python\n{code}\n```"""))
|
||||||
|
grid_.append(shoelace.Divider(vertical=True))
|
||||||
|
grid_.append(div(widget_code, style=styles.STYLE_CODE_BLOCK))
|
||||||
|
|
||||||
|
return grid_
|
||||||
|
|
||||||
|
|
||||||
|
def create_main_area():
|
||||||
|
"""Create the main area of the right side of page, with the description of the
|
||||||
|
demo itself and how to use it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the main area
|
||||||
|
|
||||||
|
"""
|
||||||
|
div_ = div(
|
||||||
|
[
|
||||||
|
h1("Welcome to PyWeb UI!", style={"text-align": "center"}),
|
||||||
|
markdown(MAIN_PAGE_MARKDOWN),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
main = el.main(
|
||||||
|
style={
|
||||||
|
"padding-top": "4rem",
|
||||||
|
"padding-bottom": "7rem",
|
||||||
|
"max-width": "52rem",
|
||||||
|
"margin-left": "auto",
|
||||||
|
"margin-right": "auto",
|
||||||
|
"padding-left": "1.5rem",
|
||||||
|
"padding-right": "1.5rem",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
main.append(div_)
|
||||||
|
|
||||||
|
return main
|
||||||
|
|
||||||
|
|
||||||
|
def create_basic_components_page(label, kit_name):
|
||||||
|
"""Create the basic components page.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the main area
|
||||||
|
|
||||||
|
"""
|
||||||
|
div_ = div(h2(label))
|
||||||
|
|
||||||
|
for component_label, component in examples.kits[kit_name].items():
|
||||||
|
div_.append(h3(component_label))
|
||||||
|
div_.append(create_component_example(component["instance"], component["code"]))
|
||||||
|
|
||||||
|
return div_
|
||||||
|
|
||||||
|
|
||||||
|
# ********** CREATE ALL THE LAYOUT **********
|
||||||
|
|
||||||
|
main_grid = grid("140px 20px auto", style={"min-height": "100%"})
|
||||||
|
|
||||||
|
# ********** MAIN PANEL **********
|
||||||
|
main_area = create_main_area()
|
||||||
|
|
||||||
|
|
||||||
|
def write_to_main(content):
|
||||||
|
main_area.html = ""
|
||||||
|
main_area.append(content)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_home():
|
||||||
|
write_to_main(create_main_area())
|
||||||
|
|
||||||
|
|
||||||
|
def basic_components():
|
||||||
|
write_to_main(
|
||||||
|
create_basic_components_page(label="Basic Components", kit_name="elements")
|
||||||
|
)
|
||||||
|
# Make sure we highlight the code
|
||||||
|
window.hljs.highlightAll()
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_components():
|
||||||
|
write_to_main(create_basic_components_page(label="", kit_name="markdown"))
|
||||||
|
|
||||||
|
|
||||||
|
def create_new_section(title, parent_div):
|
||||||
|
basic_components_text = h3(
|
||||||
|
title, style={"text-align": "left", "margin": "20px auto 0"}
|
||||||
|
)
|
||||||
|
parent_div.append(basic_components_text)
|
||||||
|
parent_div.append(
|
||||||
|
shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"})
|
||||||
|
)
|
||||||
|
return basic_components_text
|
||||||
|
|
||||||
|
|
||||||
|
# ********** LEFT PANEL **********
|
||||||
|
left_div = div()
|
||||||
|
left_panel_title = h1(
|
||||||
|
"PyWeb.UI", style={"text-align": "center", "margin": "20px auto 30px"}
|
||||||
|
)
|
||||||
|
left_div.append(left_panel_title)
|
||||||
|
left_div.append(shoelace.Divider(style={"margin-bottom": "30px"}))
|
||||||
|
# Let's map the creation of the main area to when the user clocks on "Components"
|
||||||
|
when("click", left_panel_title)(restore_home)
|
||||||
|
|
||||||
|
# BASIC COMPONENTS
|
||||||
|
basic_components_text = h3(
|
||||||
|
"Basic Components",
|
||||||
|
style={"text-align": "left", "margin": "20px auto 0", "cursor": "pointer"},
|
||||||
|
)
|
||||||
|
left_div.append(basic_components_text)
|
||||||
|
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||||
|
# Let's map the creation of the main area to when the user clocks on "Components"
|
||||||
|
when("click", basic_components_text)(basic_components)
|
||||||
|
|
||||||
|
# MARKDOWN COMPONENTS
|
||||||
|
markdown_title = create_new_section("Markdown", left_div)
|
||||||
|
when("click", markdown_title)(markdown_components)
|
||||||
|
|
||||||
|
|
||||||
|
# SHOELACE COMPONENTS
|
||||||
|
shoe_components_text = h3(
|
||||||
|
"Shoe Components", style={"text-align": "left", "margin": "20px auto 0"}
|
||||||
|
)
|
||||||
|
left_div.append(shoe_components_text)
|
||||||
|
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||||
|
|
||||||
|
# Create the links to the components on th left panel
|
||||||
|
print("SHOELACE EXAMPLES", examples.kits["shoelace"])
|
||||||
|
for component_label, component in examples.kits["shoelace"].items():
|
||||||
|
add_component_section(component_label, component, left_div)
|
||||||
|
|
||||||
|
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||||
|
left_div.append(a("Gallery", href="gallery.html", style={"text-align": "left"}))
|
||||||
|
# ********** ADD LEFT AND MAIN PANEL TO MAIN **********
|
||||||
|
main_grid.append(left_div)
|
||||||
|
main_grid.append(shoelace.Divider(vertical=True))
|
||||||
|
main_grid.append(main_area)
|
||||||
|
pydom.body.append(main_grid)
|
||||||
300
pyscript.core/test/ui/examples.py
Normal file
300
pyscript.core/test/ui/examples.py
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
from markdown import markdown
|
||||||
|
from pyscript import when, window
|
||||||
|
from pyweb import pydom
|
||||||
|
from pyweb.ui.elements import (
|
||||||
|
a,
|
||||||
|
br,
|
||||||
|
button,
|
||||||
|
code,
|
||||||
|
div,
|
||||||
|
grid,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
img,
|
||||||
|
input_,
|
||||||
|
p,
|
||||||
|
small,
|
||||||
|
strong,
|
||||||
|
)
|
||||||
|
from shoelace import (
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CopyButton,
|
||||||
|
Details,
|
||||||
|
Dialog,
|
||||||
|
Divider,
|
||||||
|
Icon,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Range,
|
||||||
|
Rating,
|
||||||
|
RelativeTime,
|
||||||
|
Skeleton,
|
||||||
|
Spinner,
|
||||||
|
Switch,
|
||||||
|
Tag,
|
||||||
|
Textarea,
|
||||||
|
)
|
||||||
|
|
||||||
|
LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||||
|
details_code = """
|
||||||
|
LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||||
|
Details(LOREM_IPSUM, summary="Try me")
|
||||||
|
"""
|
||||||
|
example_dialog_close_btn = Button("Close")
|
||||||
|
example_dialog = Dialog(div([p(LOREM_IPSUM), example_dialog_close_btn]), label="Try me")
|
||||||
|
example_dialog_btn = Button("Open Dialog")
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_dialog():
|
||||||
|
example_dialog.open = not (example_dialog.open)
|
||||||
|
|
||||||
|
|
||||||
|
when("click", example_dialog_btn)(toggle_dialog)
|
||||||
|
when("click", example_dialog_close_btn)(toggle_dialog)
|
||||||
|
|
||||||
|
pydom.body.append(example_dialog)
|
||||||
|
|
||||||
|
|
||||||
|
# ELEMENTS
|
||||||
|
|
||||||
|
# Button
|
||||||
|
btn = button("Click me!")
|
||||||
|
when("click", btn)(lambda: window.alert("Clicked!"))
|
||||||
|
|
||||||
|
# Inputs
|
||||||
|
inputs_div = div()
|
||||||
|
inputs_code = []
|
||||||
|
for input_type in [
|
||||||
|
"text",
|
||||||
|
"password",
|
||||||
|
"email",
|
||||||
|
"number",
|
||||||
|
"date",
|
||||||
|
"time",
|
||||||
|
"color",
|
||||||
|
"range",
|
||||||
|
]:
|
||||||
|
inputs_div.append(input_(type=input_type, style={"display": "block"}))
|
||||||
|
inputs_code.append(f"input_(type='{input_type}')")
|
||||||
|
|
||||||
|
|
||||||
|
headers_div = div()
|
||||||
|
headers_code = []
|
||||||
|
for header in [h1, h2, h3, h4, h5, h6]:
|
||||||
|
headers_div.append(header(f"{header.tag.upper()} header"))
|
||||||
|
headers_code.append(f'{header.tag}("{header.tag.upper()} header")')
|
||||||
|
headers_code = "\n".join(headers_code)
|
||||||
|
|
||||||
|
rich_input = input_(
|
||||||
|
type="text",
|
||||||
|
name="some name",
|
||||||
|
autofocus=True,
|
||||||
|
pattern="\w{3,16}",
|
||||||
|
placeholder="add text with > 3 chars",
|
||||||
|
required=True,
|
||||||
|
size="20",
|
||||||
|
)
|
||||||
|
inputs_div.append(rich_input)
|
||||||
|
inputs_code.append("# You can create inputs with more options like")
|
||||||
|
inputs_code.append("# this by passing properties as kwargs")
|
||||||
|
inputs_code.append(
|
||||||
|
"input_(type='text', name='some name', autofocus=True, pattern='\\w{3,16}', placeholder='add text with > 3 chars', required=True, size='20')"
|
||||||
|
)
|
||||||
|
inputs_code = "\n".join(inputs_code)
|
||||||
|
|
||||||
|
MARKDOWN_EXAMPLE = """# This is a header
|
||||||
|
|
||||||
|
This is a ~~paragraph~~ text with **bold** and *italic* text in it!
|
||||||
|
"""
|
||||||
|
|
||||||
|
kits = {
|
||||||
|
"shoelace": {
|
||||||
|
"Alert": {
|
||||||
|
"instance": Alert(
|
||||||
|
"This is a standard alert. You can customize its content and even the icon."
|
||||||
|
),
|
||||||
|
"code": "Alert('This is a standard alert. You can customize its content and even the icon.'",
|
||||||
|
},
|
||||||
|
"Icon": {
|
||||||
|
"instance": Icon(name="heart"),
|
||||||
|
"code": 'Icon(name="heart")',
|
||||||
|
},
|
||||||
|
"Button": {
|
||||||
|
"instance": Button("Try me"),
|
||||||
|
"code": 'Button("Try me")',
|
||||||
|
},
|
||||||
|
"Card": {
|
||||||
|
"instance": Card(
|
||||||
|
p("This is a cool card!"),
|
||||||
|
image="https://pyscript.net/assets/images/pyscript-sticker-black.svg",
|
||||||
|
footer=div([Button("More Info"), Rating()]),
|
||||||
|
),
|
||||||
|
"code": """
|
||||||
|
Card(p("This is a cool card!"), image="https://pyscript.net/assets/images/pyscript-sticker-black.svg", footer=div([Button("More Info"), Rating()]))
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
"Details": {
|
||||||
|
"instance": Details(LOREM_IPSUM, summary="Try me"),
|
||||||
|
"code": 'Details(LOREM_IPSUM, summary="Try me")',
|
||||||
|
},
|
||||||
|
"Dialog": {
|
||||||
|
"instance": example_dialog_btn,
|
||||||
|
"code": 'Dialog(div([p(LOREM_IPSUM), Button("Close")]), summary="Try me")',
|
||||||
|
},
|
||||||
|
"Divider": {
|
||||||
|
"instance": Divider(),
|
||||||
|
"code": "Divider()",
|
||||||
|
},
|
||||||
|
"Rating": {
|
||||||
|
"instance": Rating(),
|
||||||
|
"code": "Rating()",
|
||||||
|
},
|
||||||
|
"Radio": {
|
||||||
|
"instance": Radio("Option 42"),
|
||||||
|
"code": code('Radio("Option 42")'),
|
||||||
|
},
|
||||||
|
"Radio Group": {
|
||||||
|
"instance": RadioGroup(
|
||||||
|
[
|
||||||
|
Radio("radio 1", name="radio 1", value=1, style={"margin": "20px"}),
|
||||||
|
Radio("radio 2", name="radio 2", value=2, style={"margin": "20px"}),
|
||||||
|
Radio("radio 3", name="radio 3", value=3, style={"margin": "20px"}),
|
||||||
|
],
|
||||||
|
label="Select an option",
|
||||||
|
),
|
||||||
|
"code": code(
|
||||||
|
"""
|
||||||
|
RadioGroup([Radio("radio 1", name="radio 1", value=1, style={"margin": "20px"}),
|
||||||
|
Radio("radio 2", name="radio 2", value=2, style={"margin": "20px"}),
|
||||||
|
Radio("radio 3", name="radio 3", value=3, style={"margin": "20px"})],
|
||||||
|
label="Select an option"),"""
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"CopyButton": {
|
||||||
|
"instance": CopyButton(
|
||||||
|
value="PyShoes!",
|
||||||
|
copy_label="Copy me!",
|
||||||
|
sucess_label="Copied, check your clipboard!",
|
||||||
|
error_label="Oops, something went wrong!",
|
||||||
|
feedback_timeout=2000,
|
||||||
|
tooltip_placement="top",
|
||||||
|
),
|
||||||
|
"code": 'CopyButton(value="PyShoes!", copy_label="Copy me!", sucess_label="Copied, check your clipboard!", error_label="Oops, something went wrong!", feedback_timeout=2000, tooltip_placement="top")',
|
||||||
|
},
|
||||||
|
"Skeleton": {
|
||||||
|
"instance": Skeleton(effect="pulse"),
|
||||||
|
"code": "Skeleton(effect='pulse')",
|
||||||
|
},
|
||||||
|
"Spinner": {
|
||||||
|
"instance": Spinner(),
|
||||||
|
"code": "Spinner()",
|
||||||
|
},
|
||||||
|
"Switch": {
|
||||||
|
"instance": Switch(name="switch", size="large"),
|
||||||
|
"code": 'Switch(name="switch", size="large")',
|
||||||
|
},
|
||||||
|
"Textarea": {
|
||||||
|
"instance": Textarea(
|
||||||
|
name="textarea",
|
||||||
|
label="Textarea",
|
||||||
|
size="medium",
|
||||||
|
help_text="This is a textarea",
|
||||||
|
resize="auto",
|
||||||
|
),
|
||||||
|
"code": 'Textarea(name="textarea", label="Textarea", size="medium", help_text="This is a textarea", resize="auto")',
|
||||||
|
},
|
||||||
|
"Tag": {
|
||||||
|
"instance": Tag("Tag", variant="primary", size="medium"),
|
||||||
|
"code": 'Tag("Tag", variant="primary", size="medium")',
|
||||||
|
},
|
||||||
|
"Range": {
|
||||||
|
"instance": Range(min=0, max=100, value=50),
|
||||||
|
"code": "Range(min=0, max=100, value=50)",
|
||||||
|
},
|
||||||
|
"RelativeTime": {
|
||||||
|
"instance": RelativeTime(date="2021-01-01T00:00:00Z"),
|
||||||
|
"code": 'RelativeTime(date="2021-01-01T00:00:00Z")',
|
||||||
|
},
|
||||||
|
# "SplitPanel": {
|
||||||
|
# "instance": SplitPanel(
|
||||||
|
# div("First panel"), div("Second panel"), orientation="vertical"
|
||||||
|
# ),
|
||||||
|
# "code": code(
|
||||||
|
# 'SplitPanel(div("First panel"), div("Second panel"), orientation="vertical")'
|
||||||
|
# ),
|
||||||
|
# },
|
||||||
|
},
|
||||||
|
"elements": {
|
||||||
|
"button": {
|
||||||
|
"instance": btn,
|
||||||
|
"code": """btn = button("Click me!")
|
||||||
|
when('click', btn)(lambda: window.alert("Clicked!"))
|
||||||
|
parentdiv.append(btn)
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
"div": {
|
||||||
|
"instance": div(
|
||||||
|
"This is a div",
|
||||||
|
style={
|
||||||
|
"text-align": "center",
|
||||||
|
"margin": "0 auto",
|
||||||
|
"background-color": "cornsilk",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"code": 'div("This is a div", style={"text-align": "center", "margin": "0 auto", "background-color": "cornsilk"})',
|
||||||
|
},
|
||||||
|
"input": {"instance": inputs_div, "code": inputs_code},
|
||||||
|
"grid": {
|
||||||
|
"instance": grid(
|
||||||
|
"30% 70%",
|
||||||
|
[
|
||||||
|
div("This is a grid", style={"background-color": "lightblue"}),
|
||||||
|
p("with 2 elements", style={"background-color": "lightyellow"}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"code": 'grid([div("This is a grid")])',
|
||||||
|
},
|
||||||
|
"headers": {"instance": headers_div, "code": headers_code},
|
||||||
|
"a": {
|
||||||
|
"instance": a(
|
||||||
|
"Click here for something awesome",
|
||||||
|
href="https://pyscript.net",
|
||||||
|
target="_blank",
|
||||||
|
),
|
||||||
|
"code": 'a("Click here for something awesome", href="https://pyscript.net", target="_blank")',
|
||||||
|
},
|
||||||
|
"br": {
|
||||||
|
"instance": div([p("This is a paragraph"), br(), p("with a line break")]),
|
||||||
|
"code": 'div([p("This is a paragraph"), br(), p("with a line break")])',
|
||||||
|
},
|
||||||
|
"img": {
|
||||||
|
"instance": img(src="./giphy_winner.gif", style={"max-width": "200px"}),
|
||||||
|
"code": 'img(src="./giphy_winner.gif", style={"max-width": "200px"})',
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"instance": code("print('Hello, World!')"),
|
||||||
|
"code": "code(\"print('Hello, World!')\")",
|
||||||
|
},
|
||||||
|
"p": {"instance": p("This is a paragraph"), "code": 'p("This is a paragraph")'},
|
||||||
|
"small": {
|
||||||
|
"instance": small("This is a small text"),
|
||||||
|
"code": 'small("This is a small text")',
|
||||||
|
},
|
||||||
|
"strong": {
|
||||||
|
"instance": strong("This is a strong text"),
|
||||||
|
"code": 'strong("This is a strong text")',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"markdown": {
|
||||||
|
"markdown": {
|
||||||
|
"instance": markdown(MARKDOWN_EXAMPLE),
|
||||||
|
"code": f'markdown("""{MARKDOWN_EXAMPLE}""")',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
31
pyscript.core/test/ui/gallery.html
Normal file
31
pyscript.core/test/ui/gallery.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>PyDom UI</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/lib/marked.umd.min.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
|
||||||
|
<!-- and it's easy to individually load additional languages -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
|
||||||
|
|
||||||
|
<script>hljs.highlightAll();</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script type="mpy" src="./gallery.py" config="./pyscript.toml"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
180
pyscript.core/test/ui/gallery.py
Normal file
180
pyscript.core/test/ui/gallery.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
try:
|
||||||
|
from textwrap import dedent
|
||||||
|
except ImportError:
|
||||||
|
dedent = lambda x: x
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
import shoelace
|
||||||
|
import styles
|
||||||
|
import tictactoe
|
||||||
|
from markdown import markdown
|
||||||
|
from pyscript import when, window
|
||||||
|
from pyweb import pydom
|
||||||
|
from pyweb.ui import elements as el
|
||||||
|
|
||||||
|
MAIN_PAGE_MARKDOWN = dedent(
|
||||||
|
"""
|
||||||
|
This gallery is a collection of demos using the PyWeb.UI library. There are meant
|
||||||
|
to be examples of how to use the library to create GUI applications using Python
|
||||||
|
only.
|
||||||
|
|
||||||
|
## How to use the gallery
|
||||||
|
|
||||||
|
Simply click on the demo you want to see and the details will appear on the right
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# First thing we do is to load all the external resources we need
|
||||||
|
shoelace.load_resources()
|
||||||
|
|
||||||
|
|
||||||
|
def add_demo(demo_name, demo_creator_cb, parent_div, source=None):
|
||||||
|
"""Create a link to a component and add it to the left panel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component (str): The name of the component to add.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the component created
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Create the component link element
|
||||||
|
div = el.div(el.a(demo_name, href="#"), style=styles.STYLE_LEFT_PANEL_LINKS)
|
||||||
|
|
||||||
|
# Create a handler that opens the component details when the link is clicked
|
||||||
|
@when("click", div)
|
||||||
|
def _change():
|
||||||
|
if source:
|
||||||
|
demo_div = el.grid("50% 50%")
|
||||||
|
demo_div.append(demo_creator_cb())
|
||||||
|
widget_code = markdown(dedent(f"""```python\n{source}\n```"""))
|
||||||
|
demo_div.append(el.div(widget_code, style=styles.STYLE_CODE_BLOCK))
|
||||||
|
else:
|
||||||
|
demo_div = demo_creator_cb()
|
||||||
|
demo_div.style["margin"] = "20px"
|
||||||
|
write_to_main(demo_div)
|
||||||
|
window.hljs.highlightAll()
|
||||||
|
|
||||||
|
# Add the new link element to the parent div (left panel)
|
||||||
|
parent_div.append(div)
|
||||||
|
return div
|
||||||
|
|
||||||
|
|
||||||
|
def create_main_area():
|
||||||
|
"""Create the main area of the right side of page, with the description of the
|
||||||
|
demo itself and how to use it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the main area
|
||||||
|
|
||||||
|
"""
|
||||||
|
return el.div(
|
||||||
|
[
|
||||||
|
el.h1("PyWeb UI Gallery", style={"text-align": "center"}),
|
||||||
|
markdown(MAIN_PAGE_MARKDOWN),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_markdown_app():
|
||||||
|
"""Create the basic components page.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the main area
|
||||||
|
|
||||||
|
"""
|
||||||
|
translate_button = shoelace.Button("Convert", variant="primary")
|
||||||
|
markdown_txt_area = shoelace.TextArea(label="Use this to write your Markdown")
|
||||||
|
result_div = el.div(style=styles.STYLE_MARKDOWN_RESULT)
|
||||||
|
|
||||||
|
@when("click", translate_button)
|
||||||
|
def translate_markdown():
|
||||||
|
result_div.html = markdown(markdown_txt_area.value).html
|
||||||
|
|
||||||
|
return el.div(
|
||||||
|
[
|
||||||
|
el.h2("Markdown"),
|
||||||
|
markdown_txt_area,
|
||||||
|
translate_button,
|
||||||
|
result_div,
|
||||||
|
],
|
||||||
|
style={"margin": "20px"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ********** MAIN PANEL **********
|
||||||
|
main_area = create_main_area()
|
||||||
|
|
||||||
|
|
||||||
|
def write_to_main(content):
|
||||||
|
main_area.html = ""
|
||||||
|
main_area.append(content)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_home():
|
||||||
|
write_to_main(create_main_area())
|
||||||
|
|
||||||
|
|
||||||
|
def create_new_section(title, parent_div):
|
||||||
|
basic_components_text = el.h3(
|
||||||
|
title, style={"text-align": "left", "margin": "20px auto 0"}
|
||||||
|
)
|
||||||
|
parent_div.append(basic_components_text)
|
||||||
|
parent_div.append(
|
||||||
|
shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"})
|
||||||
|
)
|
||||||
|
return basic_components_text
|
||||||
|
|
||||||
|
|
||||||
|
# ********** LEFT PANEL **********
|
||||||
|
left_panel_title = el.h1("PyWeb.UI", style=styles.STYLE_LEFT_PANEL_TITLE)
|
||||||
|
left_div = el.div(
|
||||||
|
[
|
||||||
|
left_panel_title,
|
||||||
|
shoelace.Divider(style={"margin-bottom": "30px"}),
|
||||||
|
el.h3("Demos", style=styles.STYLE_LEFT_PANEL_TITLE),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Let's map the creation of the main area to when the user clocks on "Components"
|
||||||
|
when("click", left_panel_title)(restore_home)
|
||||||
|
|
||||||
|
# ------ ADD DEMOS ------
|
||||||
|
markdown_source = """
|
||||||
|
translate_button = shoelace.Button("Convert", variant="primary")
|
||||||
|
markdown_txt_area = shoelace.TextArea(label="Markdown",
|
||||||
|
help_text="Write your Mardown here and press convert to see the result",
|
||||||
|
)
|
||||||
|
result_div = el.div(style=styles.STYLE_MARKDOWN_RESULT)
|
||||||
|
@when("click", translate_button)
|
||||||
|
def translate_markdown():
|
||||||
|
result_div.html = markdown(markdown_txt_area.value).html
|
||||||
|
|
||||||
|
el.div([
|
||||||
|
el.h2("Markdown"),
|
||||||
|
markdown_txt_area,
|
||||||
|
translate_button,
|
||||||
|
result_div,
|
||||||
|
])
|
||||||
|
"""
|
||||||
|
add_demo("Markdown", create_markdown_app, left_div, source=markdown_source)
|
||||||
|
add_demo(
|
||||||
|
"Tic Tac Toe",
|
||||||
|
tictactoe.create_tic_tac_toe,
|
||||||
|
left_div,
|
||||||
|
source=inspect.getsource(tictactoe),
|
||||||
|
)
|
||||||
|
|
||||||
|
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||||
|
left_div.append(el.a("Examples", href="/test/ui/", style={"text-align": "left"}))
|
||||||
|
|
||||||
|
# ********** CREATE ALL THE LAYOUT **********
|
||||||
|
grid = el.grid("minmax(100px, 200px) 20px auto", style={"min-height": "100%"})
|
||||||
|
grid.append(left_div)
|
||||||
|
grid.append(shoelace.Divider(vertical=True))
|
||||||
|
grid.append(main_area)
|
||||||
|
|
||||||
|
pydom.body.append(grid)
|
||||||
|
pydom.body.append(el.a("Back to the main page", href="/test/ui/", target="_blank"))
|
||||||
|
pydom.body.append(el.a("Hidden!!!", href="/test/ui/", target="_blank", hidden=True))
|
||||||
39
pyscript.core/test/ui/index.html
Normal file
39
pyscript.core/test/ui/index.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>PyDom UI</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/lib/marked.umd.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
|
||||||
|
<!-- and it's easy to individually load additional languages -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
|
||||||
|
<!-- SHOWLACE CUSTOM CSS -->
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
|
||||||
|
}
|
||||||
|
|
||||||
|
input:invalid {
|
||||||
|
background-color: lightpink;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script type="mpy" src="./demo.py" config="./pyscript.toml"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
pyscript.core/test/ui/pyscript.toml
Normal file
8
pyscript.core/test/ui/pyscript.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
packages = []
|
||||||
|
|
||||||
|
[files]
|
||||||
|
"./examples.py" = "./examples.py"
|
||||||
|
"./tictactoe.py" = "./tictactoe.py"
|
||||||
|
"./styles.py" = "./styles.py"
|
||||||
|
"./shoelace.py" = "./shoelace.py"
|
||||||
|
"./markdown.py" = "./markdown.py"
|
||||||
6
pyscript.core/test/ws.spec.js
Normal file
6
pyscript.core/test/ws.spec.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('MicroPython WebSocket', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:5037/');
|
||||||
|
await page.waitForSelector('html.ok');
|
||||||
|
});
|
||||||
33
pyscript.core/test/ws/index.html
Normal file
33
pyscript.core/test/ws/index.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="mpy" worker>
|
||||||
|
from pyscript import WebSocket, document
|
||||||
|
|
||||||
|
def onopen(event):
|
||||||
|
print(event.type)
|
||||||
|
ws.send("hello")
|
||||||
|
|
||||||
|
def onmessage(event):
|
||||||
|
print(event.type, event.data)
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
def onclose(event):
|
||||||
|
print(event.type)
|
||||||
|
document.documentElement.classList.add("ok")
|
||||||
|
|
||||||
|
ws = WebSocket(
|
||||||
|
url="ws://localhost:5037/",
|
||||||
|
onopen=onopen,
|
||||||
|
onmessage=onmessage,
|
||||||
|
onclose=onclose
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
pyscript.core/test/ws/index.js
Normal file
33
pyscript.core/test/ws/index.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { serve, file } from 'bun';
|
||||||
|
|
||||||
|
import path, { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const dir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
serve({
|
||||||
|
port: 5037,
|
||||||
|
fetch(req, server) {
|
||||||
|
if (server.upgrade(req)) return;
|
||||||
|
const url = new URL(req.url);
|
||||||
|
let { pathname } = url;
|
||||||
|
if (pathname === '/') pathname = '/index.html';
|
||||||
|
else if (/^\/dist\//.test(pathname)) pathname = `/../..${pathname}`;
|
||||||
|
else if (pathname === '/favicon.ico')
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
const response = new Response(file(`${dir}${pathname}`));
|
||||||
|
const { headers } = response;
|
||||||
|
headers.set('Cross-Origin-Opener-Policy', 'same-origin');
|
||||||
|
headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||||
|
headers.set('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
websocket: {
|
||||||
|
message(ws, message) {
|
||||||
|
ws.send(message);
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -186,12 +186,12 @@ class TestSupport(PyScriptTest):
|
|||||||
#
|
#
|
||||||
msg = str(exc.value)
|
msg = str(exc.value)
|
||||||
expected = textwrap.dedent(
|
expected = textwrap.dedent(
|
||||||
"""
|
f"""
|
||||||
JS errors found: 2
|
JS errors found: 2
|
||||||
Error: error 1
|
Error: error 1
|
||||||
at https://fake_server/mytest.html:.*
|
at {self.http_server_addr}/mytest.html:.*
|
||||||
Error: error 2
|
Error: error 2
|
||||||
at https://fake_server/mytest.html:.*
|
at {self.http_server_addr}/mytest.html:.*
|
||||||
"""
|
"""
|
||||||
).strip()
|
).strip()
|
||||||
assert re.search(expected, msg)
|
assert re.search(expected, msg)
|
||||||
@@ -217,12 +217,12 @@ class TestSupport(PyScriptTest):
|
|||||||
#
|
#
|
||||||
msg = str(exc.value)
|
msg = str(exc.value)
|
||||||
expected = textwrap.dedent(
|
expected = textwrap.dedent(
|
||||||
"""
|
f"""
|
||||||
JS errors found: 2
|
JS errors found: 2
|
||||||
Error: NOT expected 2
|
Error: NOT expected 2
|
||||||
at https://fake_server/mytest.html:.*
|
at {self.http_server_addr}/mytest.html:.*
|
||||||
Error: NOT expected 4
|
Error: NOT expected 4
|
||||||
at https://fake_server/mytest.html:.*
|
at {self.http_server_addr}/mytest.html:.*
|
||||||
"""
|
"""
|
||||||
).strip()
|
).strip()
|
||||||
assert re.search(expected, msg)
|
assert re.search(expected, msg)
|
||||||
@@ -243,15 +243,15 @@ class TestSupport(PyScriptTest):
|
|||||||
#
|
#
|
||||||
msg = str(exc.value)
|
msg = str(exc.value)
|
||||||
expected = textwrap.dedent(
|
expected = textwrap.dedent(
|
||||||
"""
|
f"""
|
||||||
The following JS errors were expected but could not be found:
|
The following JS errors were expected but could not be found:
|
||||||
- this is not going to be found
|
- this is not going to be found
|
||||||
---
|
---
|
||||||
The following JS errors were raised but not expected:
|
The following JS errors were raised but not expected:
|
||||||
Error: error 1
|
Error: error 1
|
||||||
at https://fake_server/mytest.html:.*
|
at {self.http_server_addr}/mytest.html:.*
|
||||||
Error: error 2
|
Error: error 2
|
||||||
at https://fake_server/mytest.html:.*
|
at {self.http_server_addr}/mytest.html:.*
|
||||||
"""
|
"""
|
||||||
).strip()
|
).strip()
|
||||||
assert re.search(expected, msg)
|
assert re.search(expected, msg)
|
||||||
@@ -471,6 +471,8 @@ class TestSupport(PyScriptTest):
|
|||||||
Test that we capture a 404 in loading a page that does not exist.
|
Test that we capture a 404 in loading a page that does not exist.
|
||||||
"""
|
"""
|
||||||
self.goto("this_url_does_not_exist.html")
|
self.goto("this_url_does_not_exist.html")
|
||||||
assert [
|
if self.dev_server:
|
||||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
error = "Failed to load resource: the server responded with a status of 404 (File not found)"
|
||||||
] == self.console.all.lines
|
else:
|
||||||
|
error = "Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||||
|
assert [error] == self.console.all.lines
|
||||||
|
|||||||
605
pyscript.core/tests/integration/test_pyweb.py
Normal file
605
pyscript.core/tests/integration/test_pyweb.py
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .support import PyScriptTest, only_main, skip_worker
|
||||||
|
|
||||||
|
DEFAULT_ELEMENT_ATTRIBUTES = {
|
||||||
|
"accesskey": "s",
|
||||||
|
"autocapitalize": "off",
|
||||||
|
"autofocus": True,
|
||||||
|
"contenteditable": True,
|
||||||
|
"draggable": True,
|
||||||
|
"enterkeyhint": "go",
|
||||||
|
"hidden": False,
|
||||||
|
"id": "whateverid",
|
||||||
|
"lang": "br",
|
||||||
|
"nonce": "123",
|
||||||
|
"part": "part1:exposed1",
|
||||||
|
"popover": True,
|
||||||
|
"slot": "slot1",
|
||||||
|
"spellcheck": False,
|
||||||
|
"tabindex": 3,
|
||||||
|
"title": "whatevertitle",
|
||||||
|
"translate": "no",
|
||||||
|
"virtualkeyboardpolicy": "manual",
|
||||||
|
}
|
||||||
|
|
||||||
|
INTERPRETERS = ["py", "mpy"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=INTERPRETERS)
|
||||||
|
def interpreter(request):
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
class TestElements(PyScriptTest):
|
||||||
|
"""Test all elements in the pyweb.ui.elements module.
|
||||||
|
|
||||||
|
This class tests all elements in the pyweb.ui.elements module. It creates
|
||||||
|
an element of each type, both executing in the main thread and in a worker.
|
||||||
|
It runs each test for each interpreter defined in `INTERPRETERS`
|
||||||
|
|
||||||
|
Each individual element test looks for the element properties, sets a value
|
||||||
|
on each the supported properties and checks if the element was created correctly
|
||||||
|
and all it's properties were set correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expected_missing_file_errors(self):
|
||||||
|
# In fake server conditions this test will not throw an error due to missing files.
|
||||||
|
# If we want to skip the test, use:
|
||||||
|
# pytest.skip("Skipping: fake server doesn't throw 404 errors on missing local files.")
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
"Failed to load resource: the server responded with a status of 404 (File not found)"
|
||||||
|
]
|
||||||
|
if self.dev_server
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_el_and_basic_asserts(
|
||||||
|
self,
|
||||||
|
el_type,
|
||||||
|
el_text=None,
|
||||||
|
interpreter="py",
|
||||||
|
properties=None,
|
||||||
|
expected_errors=None,
|
||||||
|
additional_selector_rules=None,
|
||||||
|
):
|
||||||
|
"""Create an element with all its properties set, by running <script type=<interpreter> ... >
|
||||||
|
, and check if the element was created correctly and all its properties were set correctly.
|
||||||
|
"""
|
||||||
|
expected_errors = expected_errors or []
|
||||||
|
if not properties:
|
||||||
|
properties = {}
|
||||||
|
|
||||||
|
def parse_value(v):
|
||||||
|
if isinstance(v, bool):
|
||||||
|
return str(v)
|
||||||
|
|
||||||
|
return f"'{v}'"
|
||||||
|
|
||||||
|
attributes = ""
|
||||||
|
if el_text:
|
||||||
|
attributes += f'"{el_text}",'
|
||||||
|
|
||||||
|
if properties:
|
||||||
|
attributes += ", ".join(
|
||||||
|
[f"{k}={parse_value(v)}" for k, v in properties.items()]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Let's make sure the body of the page is clean first
|
||||||
|
body = self.page.locator("body")
|
||||||
|
assert body.inner_html() == ""
|
||||||
|
|
||||||
|
# Let's make sure the element is not in the page
|
||||||
|
element = self.page.locator(el_type)
|
||||||
|
assert not element.count()
|
||||||
|
|
||||||
|
# Let's create the element
|
||||||
|
code_ = f"""
|
||||||
|
from pyscript import when
|
||||||
|
<script type="{interpreter}">
|
||||||
|
from pyweb import pydom
|
||||||
|
from pyweb.ui.elements import {el_type}
|
||||||
|
el = {el_type}({attributes})
|
||||||
|
pydom.body.append(el)
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
self.pyscript_run(code_)
|
||||||
|
|
||||||
|
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||||
|
# check the return tag from the selector
|
||||||
|
locator_type = el_tag = el_type[:-1] if el_type.endswith("_") else el_type
|
||||||
|
if additional_selector_rules:
|
||||||
|
locator_type += f"{additional_selector_rules}"
|
||||||
|
|
||||||
|
el = self.page.locator(locator_type)
|
||||||
|
tag = el.evaluate("node => node.tagName")
|
||||||
|
assert tag == el_tag.upper()
|
||||||
|
if el_text:
|
||||||
|
assert el.inner_html() == el_text
|
||||||
|
assert el.text_content() == el_text
|
||||||
|
|
||||||
|
# if we expect specific errors, check that they are in the console
|
||||||
|
if expected_errors:
|
||||||
|
for error in expected_errors:
|
||||||
|
assert error in self.console.error.lines
|
||||||
|
else:
|
||||||
|
# if we don't expect errors, check that there are no errors
|
||||||
|
assert self.console.error.lines == []
|
||||||
|
|
||||||
|
if properties:
|
||||||
|
for k, v in properties.items():
|
||||||
|
actual_val = el.evaluate(f"node => node.{k}")
|
||||||
|
assert actual_val == v
|
||||||
|
return el
|
||||||
|
|
||||||
|
def test_a(self, interpreter):
|
||||||
|
a = self._create_el_and_basic_asserts("a", "click me", interpreter)
|
||||||
|
assert a.text_content() == "click me"
|
||||||
|
|
||||||
|
def test_abbr(self, interpreter):
|
||||||
|
abbr = self._create_el_and_basic_asserts(
|
||||||
|
"abbr", "some text", interpreter=interpreter
|
||||||
|
)
|
||||||
|
assert abbr.text_content() == "some text"
|
||||||
|
|
||||||
|
def test_address(self, interpreter):
|
||||||
|
address = self._create_el_and_basic_asserts("address", "some text", interpreter)
|
||||||
|
assert address.text_content() == "some text"
|
||||||
|
|
||||||
|
def test_area(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"shape": "poly",
|
||||||
|
"coords": "129,0,260,95,129,138",
|
||||||
|
"href": "https://developer.mozilla.org/docs/Web/HTTP",
|
||||||
|
"target": "_blank",
|
||||||
|
"alt": "HTTP",
|
||||||
|
}
|
||||||
|
# TODO: Check why click times out
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"area", interpreter=interpreter, properties=properties
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_article(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("article", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_aside(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("aside", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_audio(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"audio",
|
||||||
|
interpreter=interpreter,
|
||||||
|
properties={"src": "http://localhost:8080/somefile.ogg", "controls": True},
|
||||||
|
expected_errors=self.expected_missing_file_errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_b(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("aside", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_blockquote(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("blockquote", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_br(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("br", interpreter=interpreter)
|
||||||
|
|
||||||
|
def test_element_button(self, interpreter):
|
||||||
|
button = self._create_el_and_basic_asserts("button", "click me", interpreter)
|
||||||
|
assert button.inner_html() == "click me"
|
||||||
|
|
||||||
|
def test_element_button_attributes(self, interpreter):
|
||||||
|
button = self._create_el_and_basic_asserts(
|
||||||
|
"button", "click me", interpreter, None
|
||||||
|
)
|
||||||
|
assert button.inner_html() == "click me"
|
||||||
|
|
||||||
|
def test_canvas(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"height": 100,
|
||||||
|
"width": 120,
|
||||||
|
}
|
||||||
|
# TODO: Check why click times out
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"canvas", "alt text for canvas", interpreter, properties=properties
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_caption(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("caption", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_cite(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("cite", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_code(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("code", "import pyweb", interpreter)
|
||||||
|
|
||||||
|
def test_data(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"data", "some text", interpreter, properties={"value": "123"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_datalist(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("datalist", "some items", interpreter)
|
||||||
|
|
||||||
|
def test_dd(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("dd", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_del_(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"del_", "some text", interpreter, properties={"cite": "http://example.com/"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_details(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"details", "some text", interpreter, properties={"open": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_dialog(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"dialog", "some text", interpreter, properties={"open": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_div(self, interpreter):
|
||||||
|
div = self._create_el_and_basic_asserts("div", "click me", interpreter)
|
||||||
|
assert div.inner_html() == "click me"
|
||||||
|
|
||||||
|
def test_dl(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("dl", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_dt(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("dt", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_em(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("em", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_embed(self, interpreter):
|
||||||
|
# NOTE: Types actually matter and embed expects a string for height and width
|
||||||
|
# while other elements expect an int
|
||||||
|
|
||||||
|
# TODO: It's important that we add typing soon to help with the user experience
|
||||||
|
properties = {
|
||||||
|
"src": "http://localhost:8080/somefile.ogg",
|
||||||
|
"type": "video/ogg",
|
||||||
|
"width": "250",
|
||||||
|
"height": "200",
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"embed",
|
||||||
|
interpreter=interpreter,
|
||||||
|
properties=properties,
|
||||||
|
expected_errors=self.expected_missing_file_errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_fieldset(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"fieldset", "some text", interpreter, properties={"name": "some name"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_figcaption(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("figcaption", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_figure(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("figure", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_footer(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("footer", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_form(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"action": "https://example.com/submit",
|
||||||
|
"method": "post",
|
||||||
|
"name": "some name",
|
||||||
|
"autocomplete": "on",
|
||||||
|
"rel": "external",
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"form", "some text", interpreter, properties=properties
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_h1(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("h1", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_h2(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("h2", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_h3(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("h3", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_h4(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("h4", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_h5(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("h5", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_h6(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("h6", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_header(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("header", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_hgroup(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("hgroup", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_hr(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("hr", interpreter=interpreter)
|
||||||
|
|
||||||
|
def test_i(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("i", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_iframe(self, interpreter):
|
||||||
|
# TODO: same comment about defining the right types
|
||||||
|
properties = {
|
||||||
|
"src": "http://localhost:8080/somefile.html",
|
||||||
|
"width": "250",
|
||||||
|
"height": "200",
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"iframe",
|
||||||
|
interpreter,
|
||||||
|
properties=properties,
|
||||||
|
expected_errors=self.expected_missing_file_errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_img(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"src": "http://localhost:8080/somefile.png",
|
||||||
|
"alt": "some image",
|
||||||
|
"width": 250,
|
||||||
|
"height": 200,
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"img",
|
||||||
|
interpreter=interpreter,
|
||||||
|
properties=properties,
|
||||||
|
expected_errors=self.expected_missing_file_errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_input(self, interpreter):
|
||||||
|
# TODO: we need multiple input tests
|
||||||
|
properties = {
|
||||||
|
"type": "text",
|
||||||
|
"value": "some value",
|
||||||
|
"name": "some name",
|
||||||
|
"autofocus": True,
|
||||||
|
"pattern": "[A-Za-z]{3}",
|
||||||
|
"placeholder": "some placeholder",
|
||||||
|
"required": True,
|
||||||
|
"size": 20,
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"input_", interpreter=interpreter, properties=properties
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ins(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"ins", "some text", interpreter, properties={"cite": "http://example.com/"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_kbd(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("kbd", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_label(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("label", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_legend(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("legend", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_li(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("li", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_link(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"href": "http://localhost:8080/somefile.css",
|
||||||
|
"rel": "stylesheet",
|
||||||
|
"type": "text/css",
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"link",
|
||||||
|
interpreter=interpreter,
|
||||||
|
properties=properties,
|
||||||
|
expected_errors=self.expected_missing_file_errors,
|
||||||
|
additional_selector_rules="[href='http://localhost:8080/somefile.css']",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_main(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("main", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_map(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"map_", "some text", interpreter, properties={"name": "somemap"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_mark(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("mark", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_menu(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("menu", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_meter(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"value": 50,
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"low": 30,
|
||||||
|
"high": 80,
|
||||||
|
"optimum": 50,
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"meter", "some text", interpreter, properties=properties
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_nav(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("nav", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_object(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"data": "http://localhost:8080/somefile.swf",
|
||||||
|
"type": "application/x-shockwave-flash",
|
||||||
|
"width": "250",
|
||||||
|
"height": "200",
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"object_",
|
||||||
|
interpreter=interpreter,
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ol(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("ol", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_optgroup(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"optgroup", "some text", interpreter, properties={"label": "some label"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_option(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"option", "some text", interpreter, properties={"value": "some value"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_output(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("output", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_p(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("p", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_picture(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("picture", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_pre(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("pre", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_progress(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"value": 50,
|
||||||
|
"max": 100,
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"progress", "some text", interpreter, properties=properties
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_q(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"q", "some text", interpreter, properties={"cite": "http://example.com/"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_s(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("s", "some text", interpreter)
|
||||||
|
|
||||||
|
# def test_script(self):
|
||||||
|
# self._create_el_and_basic_asserts("script", "some text")
|
||||||
|
|
||||||
|
def test_section(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("section", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_select(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("select", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_small(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("small", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_source(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"src": "http://localhost:8080/somefile.ogg",
|
||||||
|
"type": "audio/ogg",
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"source",
|
||||||
|
interpreter=interpreter,
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_span(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("span", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_strong(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("strong", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_style(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"style",
|
||||||
|
"body {background-color: red;}",
|
||||||
|
interpreter,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sub(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("sub", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_summary(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("summary", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_sup(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("sup", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_table(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("table", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_tbody(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("tbody", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_td(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("td", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_template(self, interpreter):
|
||||||
|
# We are not checking the content of template since it's sort of
|
||||||
|
# special element
|
||||||
|
self._create_el_and_basic_asserts("template", interpreter=interpreter)
|
||||||
|
|
||||||
|
def test_textarea(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("textarea", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_tfoot(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("tfoot", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_th(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("th", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_thead(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("thead", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_time(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("time", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_title(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("title", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_tr(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("tr", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_track(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"src": "http://localhost:8080/somefile.vtt",
|
||||||
|
"kind": "subtitles",
|
||||||
|
"srclang": "en",
|
||||||
|
"label": "English",
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"track",
|
||||||
|
interpreter=interpreter,
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_u(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("u", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_ul(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("ul", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_var(self, interpreter):
|
||||||
|
self._create_el_and_basic_asserts("var", "some text", interpreter)
|
||||||
|
|
||||||
|
def test_video(self, interpreter):
|
||||||
|
properties = {
|
||||||
|
"src": "http://localhost:8080/somefile.ogg",
|
||||||
|
"controls": True,
|
||||||
|
"width": 250,
|
||||||
|
"height": 200,
|
||||||
|
}
|
||||||
|
self._create_el_and_basic_asserts(
|
||||||
|
"video",
|
||||||
|
interpreter=interpreter,
|
||||||
|
properties=properties,
|
||||||
|
expected_errors=self.expected_missing_file_errors,
|
||||||
|
)
|
||||||
4
pyscript.core/types/3rd-party/xterm_addon-web-links.d.ts
vendored
Normal file
4
pyscript.core/types/3rd-party/xterm_addon-web-links.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare var r: any;
|
||||||
|
declare var n: any;
|
||||||
|
declare var t: {};
|
||||||
|
export { r as WebLinksAddon, n as __esModule, t as default };
|
||||||
27
pyscript.core/types/core.d.ts
vendored
27
pyscript.core/types/core.d.ts
vendored
@@ -1,16 +1,32 @@
|
|||||||
|
export function offline_interpreter(config: any): string;
|
||||||
|
import { stdlib } from "./stdlib.js";
|
||||||
|
import { optional } from "./stdlib.js";
|
||||||
|
import { inputFailure } from "./hooks.js";
|
||||||
import TYPES from "./types.js";
|
import TYPES from "./types.js";
|
||||||
/**
|
/**
|
||||||
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||||
* @param {string} file the python file to run ina worker.
|
* @param {string} file the python file to run ina worker.
|
||||||
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
||||||
* @returns {Worker & {sync: ProxyHandler<object>}}
|
* @returns {Promise<Worker & {sync: object}>}
|
||||||
*/
|
*/
|
||||||
declare function exportedPyWorker(file: string, options?: {
|
declare function exportedPyWorker(file: string, options?: {
|
||||||
config?: string | object;
|
config?: string | object;
|
||||||
async?: boolean;
|
async?: boolean;
|
||||||
}): Worker & {
|
}): Promise<Worker & {
|
||||||
sync: ProxyHandler<object>;
|
sync: object;
|
||||||
};
|
}>;
|
||||||
|
/**
|
||||||
|
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||||
|
* @param {string} file the python file to run ina worker.
|
||||||
|
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
||||||
|
* @returns {Promise<Worker & {sync: object}>}
|
||||||
|
*/
|
||||||
|
declare function exportedMPWorker(file: string, options?: {
|
||||||
|
config?: string | object;
|
||||||
|
async?: boolean;
|
||||||
|
}): Promise<Worker & {
|
||||||
|
sync: object;
|
||||||
|
}>;
|
||||||
declare const exportedHooks: {
|
declare const exportedHooks: {
|
||||||
main: {
|
main: {
|
||||||
onWorker: Set<Function>;
|
onWorker: Set<Function>;
|
||||||
@@ -38,5 +54,4 @@ declare const exportedHooks: {
|
|||||||
};
|
};
|
||||||
declare const exportedConfig: {};
|
declare const exportedConfig: {};
|
||||||
declare const exportedWhenDefined: (type: string) => Promise<any>;
|
declare const exportedWhenDefined: (type: string) => Promise<any>;
|
||||||
import sync from "./sync.js";
|
export { stdlib, optional, inputFailure, TYPES, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
||||||
export { TYPES, exportedPyWorker as PyWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
|
||||||
|
|||||||
3
pyscript.core/types/fetch.d.ts
vendored
3
pyscript.core/types/fetch.d.ts
vendored
@@ -8,5 +8,4 @@
|
|||||||
* @returns {Promise<Response>}
|
* @returns {Promise<Response>}
|
||||||
*/
|
*/
|
||||||
export function robustFetch(url: string, options?: Request): Promise<Response>;
|
export function robustFetch(url: string, options?: Request): Promise<Response>;
|
||||||
export { getText };
|
export function getText(response: Response): Promise<string>;
|
||||||
import { getText } from "polyscript/exports";
|
|
||||||
|
|||||||
3
pyscript.core/types/hooks.d.ts
vendored
3
pyscript.core/types/hooks.d.ts
vendored
@@ -1,7 +1,8 @@
|
|||||||
export function main(name: any): any;
|
export function main(name: any): any;
|
||||||
export function worker(name: any): any;
|
export function worker(name: any): any;
|
||||||
export function codeFor(branch: any): {};
|
export function codeFor(branch: any, type: any): {};
|
||||||
export function createFunction(self: any, name: any): any;
|
export function createFunction(self: any, name: any): any;
|
||||||
|
export const inputFailure: "\n import builtins\n def input(prompt=\"\"):\n raise Exception(\"\\n \".join([\n \"input() doesn't work when PyScript runs in the main thread.\",\n \"Consider using the worker attribute: https://pyscript.github.io/docs/2023.11.2/user-guide/workers/\"\n ]))\n\n builtins.input = input\n del builtins\n del input\n";
|
||||||
export namespace hooks {
|
export namespace hooks {
|
||||||
namespace main {
|
namespace main {
|
||||||
let onWorker: Set<Function>;
|
let onWorker: Set<Function>;
|
||||||
|
|||||||
3
pyscript.core/types/plugins/py-terminal.d.ts
vendored
3
pyscript.core/types/plugins/py-terminal.d.ts
vendored
@@ -1,2 +1 @@
|
|||||||
declare const _default: Promise<void>;
|
export {};
|
||||||
export default _default;
|
|
||||||
|
|||||||
2
pyscript.core/types/plugins/py-terminal/mpy.d.ts
vendored
Normal file
2
pyscript.core/types/plugins/py-terminal/mpy.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
declare function _default(element: any): Promise<void>;
|
||||||
|
export default _default;
|
||||||
2
pyscript.core/types/plugins/py-terminal/py.d.ts
vendored
Normal file
2
pyscript.core/types/plugins/py-terminal/py.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
declare function _default(element: any): Promise<void>;
|
||||||
|
export default _default;
|
||||||
4
pyscript.core/types/stdlib.d.ts
vendored
4
pyscript.core/types/stdlib.d.ts
vendored
@@ -1,2 +1,2 @@
|
|||||||
declare const _default: string;
|
export const stdlib: string;
|
||||||
export default _default;
|
export const optional: string;
|
||||||
|
|||||||
7
pyscript.core/types/stdlib/pyscript.d.ts
vendored
7
pyscript.core/types/stdlib/pyscript.d.ts
vendored
@@ -3,13 +3,20 @@ declare namespace _default {
|
|||||||
"__init__.py": string;
|
"__init__.py": string;
|
||||||
"display.py": string;
|
"display.py": string;
|
||||||
"event_handling.py": string;
|
"event_handling.py": string;
|
||||||
|
"fetch.py": string;
|
||||||
|
"ffi.py": string;
|
||||||
"magic_js.py": string;
|
"magic_js.py": string;
|
||||||
"util.py": string;
|
"util.py": string;
|
||||||
|
"websocket.py": string;
|
||||||
};
|
};
|
||||||
let pyweb: {
|
let pyweb: {
|
||||||
"__init__.py": string;
|
"__init__.py": string;
|
||||||
"media.py": string;
|
"media.py": string;
|
||||||
"pydom.py": string;
|
"pydom.py": string;
|
||||||
|
ui: {
|
||||||
|
"__init__.py": string;
|
||||||
|
"elements.py": string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export default _default;
|
export default _default;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
black==23.11.0
|
black==24.4.2
|
||||||
isort==5.12.0
|
isort==5.13.2
|
||||||
pytest==7.1.2
|
pytest==8.2.1
|
||||||
pre-commit==3.5.0
|
pre-commit==3.7.1
|
||||||
playwright==1.33.0
|
playwright==1.44.0
|
||||||
pytest-playwright==0.3.3
|
pytest-playwright==0.5.0
|
||||||
pytest-xdist==3.3.0
|
pytest-xdist==3.6.1
|
||||||
pexpect==4.9.0
|
pexpect==4.9.0
|
||||||
pyodide_py==0.24.1
|
pyodide_py==0.24.1
|
||||||
micropip==0.5.0
|
micropip==0.5.0
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
numpy==1.26.2
|
numpy==1.26.4
|
||||||
pillow==10.1.0
|
pillow==10.3.0
|
||||||
|
|||||||
Reference in New Issue
Block a user