mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ec3381789 | ||
|
|
9bd4737708 | ||
|
|
c49cb9231b | ||
|
|
d1d1c5740f | ||
|
|
1a05ea5fd2 | ||
|
|
5b4e8527da | ||
|
|
83c2afeaf1 | ||
|
|
643b76479f | ||
|
|
cf92996071 | ||
|
|
c653296821 | ||
|
|
44cd6273ba | ||
|
|
d7d2dfb383 | ||
|
|
2d5cf096e0 | ||
|
|
6ee8217593 | ||
|
|
6d45728787 | ||
|
|
65954a627e | ||
|
|
2f1b764251 | ||
|
|
1fb6cddd70 | ||
|
|
239add4e20 | ||
|
|
4e4ac56729 | ||
|
|
1447cb3094 | ||
|
|
2f3659b676 | ||
|
|
910c666319 | ||
|
|
eee2f64c1d | ||
|
|
d080246a0f | ||
|
|
98c0f5e50d | ||
|
|
a1268f1aa2 | ||
|
|
69b8884045 | ||
|
|
df1d699fe6 | ||
|
|
84f197b657 | ||
|
|
5bed5ede52 | ||
|
|
f6d5cf06c8 | ||
|
|
30c6c830ae | ||
|
|
d7084f7f55 | ||
|
|
a87d2b3fea | ||
|
|
81a26363a3 | ||
|
|
53e945201d | ||
|
|
181d276c8b | ||
|
|
bcaab0eb93 | ||
|
|
3ff0f84391 | ||
|
|
2b411fc635 | ||
|
|
2128572ce5 | ||
|
|
63f2453091 | ||
|
|
f6470dcad5 | ||
|
|
a9717afeb7 | ||
|
|
cea52b4334 | ||
|
|
7ad7f0abfb | ||
|
|
1efd73af8f | ||
|
|
1e7fb9af44 | ||
|
|
154e00d320 |
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -11,7 +11,9 @@ body:
|
||||
|
||||
There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration!
|
||||
|
||||
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists
|
||||
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists.
|
||||
|
||||
Finally, if you are opening **a bug report related to PyScript.com** please [use this repository instead](https://github.com/anaconda/pyscript-dot-com-issues/issues/new/choose).
|
||||
|
||||
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
|
||||
- type: checkboxes
|
||||
|
||||
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
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
run: zip -r -q ./build.zip ./dist
|
||||
|
||||
- name: Prepare Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
prerelease: true
|
||||
|
||||
4
.github/workflows/publish-release.yml
vendored
4
.github/workflows/publish-release.yml
vendored
@@ -19,12 +19,12 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
|
||||
4
.github/workflows/publish-snapshot.yml
vendored
4
.github/workflows/publish-snapshot.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
|
||||
4
.github/workflows/publish-unstable.yml
vendored
4
.github/workflows/publish-unstable.yml
vendored
@@ -24,12 +24,12 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -37,12 +37,12 @@ jobs:
|
||||
run: git log --graph -3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: setup Miniconda
|
||||
uses: conda-incubator/setup-miniconda@v2
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
|
||||
- name: Create and activate virtual environment
|
||||
run: |
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
run: |
|
||||
make test-integration
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pyscript
|
||||
path: |
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: test_results
|
||||
|
||||
2
.github/workflows/test_report.yml
vendored
2
.github/workflows/test_report.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
report:
|
||||
runs-on: ubuntu-latest-8core
|
||||
steps:
|
||||
- uses: dorny/test-reporter@v1.6.0
|
||||
- uses: dorny/test-reporter@v1.9.0
|
||||
with:
|
||||
artifact: test_results
|
||||
name: Test reports
|
||||
|
||||
@@ -7,7 +7,7 @@ ci:
|
||||
default_stages: [commit]
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-builtin-literals
|
||||
- id: check-case-conflict
|
||||
@@ -25,7 +25,7 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.11.0
|
||||
rev: 24.4.2
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
|
||||
@@ -46,7 +46,7 @@ repos:
|
||||
args: [--tab-width, "4"]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
|
||||
100
CONTRIBUTING.md
100
CONTRIBUTING.md
@@ -79,3 +79,103 @@ The Project abides by the Organization's [trademark policy](https://github.com/p
|
||||
|
||||
Part of MVG-0.1-beta.
|
||||
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
# Quick guide to pytest
|
||||
|
||||
We make heavy usage of pytest. Here is a quick guide and collection of useful options:
|
||||
|
||||
- To run all tests in the current directory and subdirectories: pytest
|
||||
|
||||
- To run tests in a specific directory or file: pytest path/to/dir/test_foo.py
|
||||
|
||||
- -s: disables output capturing
|
||||
|
||||
- --pdb: in case of exception, enter a (Pdb) prompt so that you can inspect what went wrong.
|
||||
|
||||
- -v: verbose mode
|
||||
|
||||
- -x: stop the execution as soon as one test fails
|
||||
|
||||
- -k foo: run only the tests whose full name contains foo
|
||||
|
||||
- -k 'foo and bar'
|
||||
|
||||
- -k 'foo and not bar'
|
||||
|
||||
## Running integration tests under pytest
|
||||
|
||||
make test is useful to run all the tests, but during the development is useful to have more control on how tests are run. The following guide assumes that you are in the directory pyscriptjs/tests/integration/.
|
||||
|
||||
### To run all the integration tests, single or multi core
|
||||
|
||||
$ pytest -xv
|
||||
...
|
||||
|
||||
test_00_support.py::TestSupport::test_basic[chromium] PASSED [ 0%]
|
||||
test_00_support.py::TestSupport::test_console[chromium] PASSED [ 1%]
|
||||
test_00_support.py::TestSupport::test_check_js_errors_simple[chromium] PASSED [ 2%]
|
||||
test_00_support.py::TestSupport::test_check_js_errors_expected[chromium] PASSED [ 3%]
|
||||
test_00_support.py::TestSupport::test_check_js_errors_expected_but_didnt_raise[chromium] PASSED [ 4%]
|
||||
test_00_support.py::TestSupport::test_check_js_errors_multiple[chromium] PASSED [ 5%]
|
||||
...
|
||||
|
||||
-x means "stop at the first failure". -v means "verbose", so that you can see all the test names one by one. We try to keep tests in a reasonable order, from most basic to most complex. This way, if you introduced some bug in very basic things, you will notice immediately.
|
||||
|
||||
If you have the pytest-xdist plugin installed, you can run all the integration tests on 4 cores in parallel:
|
||||
|
||||
$ pytest -n 4
|
||||
|
||||
### To run a single test, headless
|
||||
|
||||
$ pytest test_01_basic.py -k test_pyscript_hello -s
|
||||
...
|
||||
[ 0.00 page.goto ] pyscript_hello.html
|
||||
[ 0.01 request ] 200 - fake_server - http://fake_server/pyscript_hello.html
|
||||
...
|
||||
[ 0.17 console.info ] [py-loader] Downloading pyodide-x.y.z...
|
||||
[ 0.18 request ] 200 - CACHED - https://cdn.jsdelivr.net/pyodide/vx.y.z/full/pyodide.js
|
||||
...
|
||||
[ 3.59 console.info ] [pyscript/main] PyScript page fully initialized
|
||||
[ 3.60 console.log ] hello pyscript
|
||||
|
||||
-k selects tests by pattern matching as described above. -s instructs pytest to show the output to the terminal instead of capturing it. In the output you can see various useful things, including network requests and JS console messages.
|
||||
|
||||
### To run a single test, headed
|
||||
|
||||
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed
|
||||
...
|
||||
|
||||
Same as above, but with --headed the browser is shown in a window, and you can interact with it. The browser uses a fake server, which means that HTTP requests are cached.
|
||||
|
||||
Unfortunately, in this mode source maps does not seem to work, and you cannot debug the original typescript source code. This seems to be a bug in playwright, for which we have a workaround:
|
||||
|
||||
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed --no-fake-server
|
||||
...
|
||||
|
||||
As the name implies, -no-fake-server disables the fake server: HTTP requests are not cached, but source-level debugging works.
|
||||
|
||||
Finally:
|
||||
|
||||
$ pytest test_01_basic.py -k test_pyscript_hello -s --dev
|
||||
...
|
||||
|
||||
--dev implies --headed --no-fake-server. In addition, it also automatically open chrome dev tools.
|
||||
|
||||
### To run only main thread or worker tests
|
||||
|
||||
By default, we run each test twice: one with execution_thread = "main" and one with execution_thread = "worker". If you want to run only half of them, you can use -m:
|
||||
|
||||
$ pytest -m main # run only the tests in the main thread
|
||||
$ pytest -m worker # ron only the tests in the web worker
|
||||
|
||||
## Fake server, HTTP cache
|
||||
|
||||
By default, our test machinery uses a playwright router which intercepts and cache HTTP requests, so that for example you don't have to download pyodide again and again. This also enables the possibility of running tests in parallel on multiple cores.
|
||||
|
||||
The cache is stored using the pytest-cache plugin, which means that it survives across sessions.
|
||||
|
||||
If you want to temporarily disable the cache, the easiest thing is to use --no-fake-server, which bypasses it completely.
|
||||
|
||||
If you want to clear the cache, you can use the special option --clear-http-cache:
|
||||
|
||||
NOTE: this works only if you are inside tests/integration, or if you explicitly specify tests/integration from the command line. This is due to how pytest decides to search for and load the various conftest.py.
|
||||
|
||||
6
LICENSE
6
LICENSE
@@ -186,7 +186,11 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Copyright (c) 2022-present, PyScript Development Team
|
||||
|
||||
Originated at Anaconda, Inc. in 2022
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
1007
pyscript.core/package-lock.json
generated
1007
pyscript.core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pyscript/core",
|
||||
"version": "0.3.14",
|
||||
"version": "0.4.31",
|
||||
"type": "module",
|
||||
"description": "PyScript",
|
||||
"module": "./index.js",
|
||||
@@ -20,13 +20,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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=false; 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:plugins": "node rollup/plugins.cjs",
|
||||
"build:stdlib": "node rollup/stdlib.cjs",
|
||||
"build:3rd-party": "node rollup/3rd-party.cjs",
|
||||
"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",
|
||||
"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",
|
||||
@@ -42,31 +43,33 @@
|
||||
"dependencies": {
|
||||
"@ungap/with-resolvers": "^0.1.0",
|
||||
"basic-devtools": "^0.1.6",
|
||||
"polyscript": "^0.6.8",
|
||||
"polyscript": "^0.12.8",
|
||||
"sticky-module": "^0.1.1",
|
||||
"to-json-callback": "^0.1.1",
|
||||
"type-checked-collections": "^0.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.3.2",
|
||||
"@codemirror/lang-python": "^6.1.3",
|
||||
"@codemirror/language": "^6.9.3",
|
||||
"@codemirror/state": "^6.3.3",
|
||||
"@codemirror/view": "^6.22.3",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@codemirror/commands": "^6.5.0",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.26.3",
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@webreflection/toml-j0.4": "^1.1.3",
|
||||
"@xterm/addon-fit": "^0.9.0-beta.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"bun": "^1.1.7",
|
||||
"chokidar": "^3.6.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"eslint": "^8.56.0",
|
||||
"rollup": "^4.9.1",
|
||||
"eslint": "^9.2.0",
|
||||
"rollup": "^4.17.2",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"static-handler": "^0.4.3",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript": "^5.4.5",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-readline": "^1.1.1"
|
||||
},
|
||||
|
||||
@@ -51,6 +51,9 @@ const modules = {
|
||||
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||
b.text(),
|
||||
),
|
||||
"xterm_addon-web-links.js": fetch(
|
||||
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||
).then((b) => b.text()),
|
||||
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
||||
(b) => b.text(),
|
||||
),
|
||||
|
||||
@@ -63,6 +63,9 @@ for (const [TYPE] of TYPES) {
|
||||
/** @type {Error | undefined} The error thrown when parsing the PyScript config, if any.*/
|
||||
let error;
|
||||
|
||||
/** @type {string | undefined} The `configURL` field to normalize all config operations as opposite of guessing it once resolved */
|
||||
let configURL;
|
||||
|
||||
let config,
|
||||
type,
|
||||
pyElement,
|
||||
@@ -105,6 +108,7 @@ for (const [TYPE] of TYPES) {
|
||||
if (!error && config) {
|
||||
try {
|
||||
const { json, toml, text, url } = await configDetails(config, type);
|
||||
if (url) configURL = new URL(url, location.href).href;
|
||||
config = text;
|
||||
if (json || type === "json") {
|
||||
try {
|
||||
@@ -146,7 +150,7 @@ for (const [TYPE] of TYPES) {
|
||||
// assign plugins as Promise.all only if needed
|
||||
plugins = Promise.all(toBeAwaited);
|
||||
|
||||
configs.set(TYPE, { config: parsed, plugins, error });
|
||||
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||
}
|
||||
|
||||
export default configs;
|
||||
|
||||
@@ -26,13 +26,42 @@ import { ErrorCode } from "./exceptions.js";
|
||||
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||
import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
|
||||
|
||||
import { stdlib, optional } from "./stdlib.js";
|
||||
export { stdlib, optional };
|
||||
|
||||
// generic helper to disambiguate between custom element and 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
|
||||
const [
|
||||
{
|
||||
PyWorker: exportedPyWorker,
|
||||
MPWorker: exportedMPWorker,
|
||||
hooks: exportedHooks,
|
||||
config: exportedConfig,
|
||||
whenDefined: exportedWhenDefined,
|
||||
@@ -40,6 +69,7 @@ const [
|
||||
alreadyLive,
|
||||
] = stickyModule("@pyscript/core", {
|
||||
PyWorker,
|
||||
MPWorker,
|
||||
hooks,
|
||||
config: {},
|
||||
whenDefined,
|
||||
@@ -48,11 +78,15 @@ const [
|
||||
export {
|
||||
TYPES,
|
||||
exportedPyWorker as PyWorker,
|
||||
exportedMPWorker as MPWorker,
|
||||
exportedHooks as hooks,
|
||||
exportedConfig as config,
|
||||
exportedWhenDefined as whenDefined,
|
||||
};
|
||||
|
||||
export const offline_interpreter = (config) =>
|
||||
config?.interpreter && new URL(config.interpreter, location.href).href;
|
||||
|
||||
const hooked = new Map();
|
||||
|
||||
for (const [TYPE, interpreter] of TYPES) {
|
||||
@@ -64,7 +98,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
else dispatch(element, TYPE, "done");
|
||||
};
|
||||
|
||||
const { config, plugins, error } = configs.get(TYPE);
|
||||
const { config, configURL, plugins, error } = configs.get(TYPE);
|
||||
|
||||
// create a unique identifier when/if needed
|
||||
let id = 0;
|
||||
@@ -137,7 +171,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
// specific main and worker hooks
|
||||
const hooks = {
|
||||
main: {
|
||||
...codeFor(main),
|
||||
...codeFor(main, TYPE),
|
||||
async onReady(wrap, element) {
|
||||
registerModule(wrap);
|
||||
|
||||
@@ -234,7 +268,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
},
|
||||
},
|
||||
worker: {
|
||||
...codeFor(worker),
|
||||
...codeFor(worker, TYPE),
|
||||
// these are lazy getters that returns a composition
|
||||
// of the current hooks or undefined, if no hook is present
|
||||
get onReady() {
|
||||
@@ -259,10 +293,11 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
|
||||
define(TYPE, {
|
||||
config,
|
||||
configURL,
|
||||
interpreter,
|
||||
hooks,
|
||||
env: `${TYPE}-script`,
|
||||
version: config?.interpreter,
|
||||
version: offline_interpreter(config),
|
||||
onerror(error, element) {
|
||||
errors.set(element, error);
|
||||
},
|
||||
@@ -313,24 +348,3 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
// export the used config without allowing leaks through it
|
||||
exportedConfig[TYPE] = structuredClone(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||
* @param {string} file the python file to run ina worker.
|
||||
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
||||
* @returns {Worker & {sync: ProxyHandler<object>}}
|
||||
*/
|
||||
function PyWorker(file, options) {
|
||||
const hooks = hooked.get("py");
|
||||
// this propagates pyscript worker hooks without needing a pyscript
|
||||
// bootstrap + it passes arguments and 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 { getText } from "polyscript/exports";
|
||||
|
||||
export { getText };
|
||||
/**
|
||||
* @param {Response} response
|
||||
* @returns
|
||||
*/
|
||||
export const getText = (response) => response.text();
|
||||
|
||||
/**
|
||||
* This is a fetch wrapper that handles any non 200 responses and throws a
|
||||
|
||||
@@ -2,7 +2,7 @@ import { typedSet } from "type-checked-collections";
|
||||
import { dedent } from "polyscript/exports";
|
||||
import toJSONCallback from "to-json-callback";
|
||||
|
||||
import stdlib from "./stdlib.js";
|
||||
import { stdlib, optional } from "./stdlib.js";
|
||||
|
||||
export const main = (name) => hooks.main[name];
|
||||
export const worker = (name) => hooks.worker[name];
|
||||
@@ -15,10 +15,11 @@ const code = (hooks, branch, key, lib) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const codeFor = (branch) => {
|
||||
export const codeFor = (branch, type) => {
|
||||
const pylib = type === "mpy" ? stdlib.replace(optional, "") : stdlib;
|
||||
const hooks = {};
|
||||
code(hooks, branch, `codeBeforeRun`, stdlib);
|
||||
code(hooks, branch, `codeBeforeRunAsync`, stdlib);
|
||||
code(hooks, branch, `codeBeforeRun`, pylib);
|
||||
code(hooks, branch, `codeBeforeRunAsync`, pylib);
|
||||
code(hooks, branch, `codeAfterRun`);
|
||||
code(hooks, branch, `codeAfterRunAsync`);
|
||||
return hooks;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// PyScript py-editor plugin
|
||||
import { Hook, XWorker, dedent } 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>`;
|
||||
|
||||
@@ -8,13 +8,15 @@ let id = 0;
|
||||
const getID = (type) => `${type}-editor-${id++}`;
|
||||
|
||||
const envs = new Map();
|
||||
const configs = new Map();
|
||||
|
||||
const hooks = {
|
||||
worker: {
|
||||
codeBeforeRun: () => stdlib,
|
||||
// works on both Pyodide and MicroPython
|
||||
onReady: ({ runAsync, io }, { sync }) => {
|
||||
io.stdout = (line) => sync.write(line);
|
||||
io.stderr = (line) => sync.writeErr(line);
|
||||
io.stdout = io.buffered(sync.write);
|
||||
io.stderr = io.buffered(sync.writeErr);
|
||||
sync.revoke();
|
||||
sync.runAsync = runAsync;
|
||||
},
|
||||
@@ -23,15 +25,27 @@ const hooks = {
|
||||
|
||||
async function execute({ currentTarget }) {
|
||||
const { env, pySrc, outDiv } = this;
|
||||
const hasRunButton = !!currentTarget;
|
||||
|
||||
if (hasRunButton) {
|
||||
currentTarget.disabled = true;
|
||||
outDiv.innerHTML = "";
|
||||
}
|
||||
|
||||
if (!envs.has(env)) {
|
||||
const srcLink = URL.createObjectURL(new Blob([""]));
|
||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, {
|
||||
type: this.interpreter,
|
||||
});
|
||||
const details = { 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);
|
||||
}
|
||||
|
||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||
|
||||
const { sync } = xworker;
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
@@ -46,21 +60,25 @@ async function execute({ currentTarget }) {
|
||||
// before executing the current code
|
||||
envs.get(env).then((xworker) => {
|
||||
xworker.onerror = ({ error }) => {
|
||||
if (hasRunButton) {
|
||||
outDiv.innerHTML += `<span style='color:red'>${
|
||||
error.message || error
|
||||
}</span>\n`;
|
||||
}
|
||||
console.error(error);
|
||||
};
|
||||
|
||||
const enable = () => {
|
||||
currentTarget.disabled = false;
|
||||
if (hasRunButton) currentTarget.disabled = false;
|
||||
};
|
||||
const { sync } = xworker;
|
||||
sync.write = (str) => {
|
||||
outDiv.innerText += `${str}\n`;
|
||||
if (hasRunButton) outDiv.innerText += `${str}\n`;
|
||||
};
|
||||
sync.writeErr = (str) => {
|
||||
if (hasRunButton) {
|
||||
outDiv.innerHTML += `<span style='color:red'>${str}</span>\n`;
|
||||
}
|
||||
};
|
||||
sync.runAsync(pySrc).then(enable, enable);
|
||||
});
|
||||
@@ -120,7 +138,6 @@ const init = async (script, type, interpreter) => {
|
||||
{ keymap },
|
||||
{ defaultKeymap },
|
||||
] = 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_state.js"),
|
||||
import(
|
||||
@@ -131,6 +148,42 @@ const init = async (script, type, interpreter) => {
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
||||
]);
|
||||
|
||||
const 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);
|
||||
|
||||
const 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;
|
||||
},
|
||||
};
|
||||
|
||||
if (isSetup) {
|
||||
execute.call(context, { currentTarget: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const selector = script.getAttribute("target");
|
||||
|
||||
let target;
|
||||
@@ -149,18 +202,6 @@ const init = async (script, type, interpreter) => {
|
||||
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
|
||||
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
|
||||
const listener = execute.bind(context);
|
||||
const [boxDiv, outDiv] = makeBoxDiv(listener, type);
|
||||
@@ -200,6 +241,9 @@ const init = async (script, type, interpreter) => {
|
||||
// avoid too greedy MutationObserver operations at distance
|
||||
let timeout = 0;
|
||||
|
||||
// avoid delayed initialization
|
||||
let queue = Promise.resolve();
|
||||
|
||||
// reset interval value then check for new scripts
|
||||
const resetTimeout = () => {
|
||||
timeout = 0;
|
||||
@@ -207,17 +251,20 @@ const resetTimeout = () => {
|
||||
};
|
||||
|
||||
// triggered both ASAP on the living DOM and via MutationObserver later
|
||||
const pyEditor = async () => {
|
||||
const pyEditor = () => {
|
||||
if (timeout) return;
|
||||
timeout = setTimeout(resetTimeout, 250);
|
||||
for (const [type, interpreter] of TYPES) {
|
||||
const selector = `script[type="${type}-editor"]`;
|
||||
for (const script of document.querySelectorAll(selector)) {
|
||||
// avoid any further bootstrap
|
||||
// avoid any further bootstrap by changing the type as active
|
||||
script.type += "-active";
|
||||
await init(script, type, interpreter);
|
||||
// don't await in here or multiple calls might happen
|
||||
// while the first script is being initialized
|
||||
queue = queue.then(() => init(script, type, interpreter));
|
||||
}
|
||||
}
|
||||
return queue;
|
||||
};
|
||||
|
||||
new MutationObserver(pyEditor).observe(document, {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// PyScript py-terminal plugin
|
||||
import { TYPES, hooks } from "../core.js";
|
||||
import { notify } from "./error.js";
|
||||
import { defineProperty } from "polyscript/exports";
|
||||
import { customObserver, defineProperties } from "polyscript/exports";
|
||||
|
||||
const SELECTOR = [...TYPES.keys()]
|
||||
.map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`)
|
||||
.join(",");
|
||||
// will contain all valid selectors
|
||||
const SELECTORS = [];
|
||||
|
||||
// show the error on main and
|
||||
// stops the module from keep executing
|
||||
@@ -14,37 +13,135 @@ const notifyAndThrow = (message) => {
|
||||
throw new Error(message);
|
||||
};
|
||||
|
||||
const pyTerminal = async () => {
|
||||
const terminals = document.querySelectorAll(SELECTOR);
|
||||
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
||||
|
||||
// no results will look further for runtime nodes
|
||||
if (!terminals.length) return;
|
||||
const bootstrapped = new WeakSet();
|
||||
|
||||
// if we arrived this far, let's drop the MutationObserver
|
||||
// as we only support one terminal per page (right now).
|
||||
mo.disconnect();
|
||||
let addStyle = true;
|
||||
|
||||
// we currently support only one terminal as in "classic"
|
||||
if (terminals.length > 1) notifyAndThrow("You can use at most 1 terminal.");
|
||||
// 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 (!sync.is_pyterminal()) return;
|
||||
|
||||
const [element] = terminals;
|
||||
// hopefully to be removed in the near future!
|
||||
if (element.matches('script[type="mpy"],mpy-script'))
|
||||
notifyAndThrow("Unsupported terminal.");
|
||||
|
||||
// import styles lazily
|
||||
document.head.append(
|
||||
Object.assign(document.createElement("link"), {
|
||||
rel: "stylesheet",
|
||||
href: new URL("./xterm.css", import.meta.url),
|
||||
}),
|
||||
// in workers it's always safe to grab the polyscript currentScript
|
||||
// the ugly `_` dance is due MicroPython not able to import via:
|
||||
// `from polyscript.currentScript import terminal as __terminal__`
|
||||
run(
|
||||
"from polyscript import currentScript as _; __terminal__ = _.terminal; del _",
|
||||
);
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
// This part works already in both Pyodide and MicroPython
|
||||
io.stderr = (error) => {
|
||||
pyterminal_write(String(error.message || error));
|
||||
};
|
||||
|
||||
// MicroPython has no code or code.interact()
|
||||
// This part patches it in a way that simulates
|
||||
// the code.interact() module in Pyodide.
|
||||
if (type === "mpy") {
|
||||
// monkey patch global input otherwise broken in MicroPython
|
||||
interpreter.registerJsModule("_pyscript_input", {
|
||||
input: pyterminal_read,
|
||||
});
|
||||
run("from _pyscript_input import input");
|
||||
|
||||
// this is needed to avoid truncated unicode in MicroPython
|
||||
// the reason is that `linebuffer` false just send one byte
|
||||
// per time and readline here doesn't like it much.
|
||||
// MicroPython also has issues with code-points and
|
||||
// replProcessChar(byte) but that function accepts only
|
||||
// one byte per time so ... we have an issue!
|
||||
// @see https://github.com/pyscript/pyscript/pull/2018
|
||||
// @see https://github.com/WebReflection/buffer-points
|
||||
const bufferPoints = (stdio) => {
|
||||
const bytes = [];
|
||||
let needed = 0;
|
||||
return (buffer) => {
|
||||
let written = 0;
|
||||
for (const byte of buffer) {
|
||||
bytes.push(byte);
|
||||
// @see https://encoding.spec.whatwg.org/#utf-8-bytes-needed
|
||||
if (needed) needed--;
|
||||
else if (0xc2 <= byte && byte <= 0xdf) needed = 1;
|
||||
else if (0xe0 <= byte && byte <= 0xef) needed = 2;
|
||||
else if (0xf0 <= byte && byte <= 0xf4) needed = 3;
|
||||
if (!needed) {
|
||||
written += bytes.length;
|
||||
stdio(new Uint8Array(bytes.splice(0)));
|
||||
}
|
||||
}
|
||||
return written;
|
||||
};
|
||||
};
|
||||
|
||||
io.stdout = bufferPoints(generic.write);
|
||||
|
||||
// tiny shim of the code module with only interact
|
||||
// to bootstrap a REPL like environment
|
||||
interpreter.registerJsModule("code", {
|
||||
interact() {
|
||||
let input = "";
|
||||
let length = 1;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const acc = [];
|
||||
const handlePoints = bufferPoints((buffer) => {
|
||||
acc.push(...buffer);
|
||||
pyterminal_write(decoder.decode(buffer));
|
||||
});
|
||||
|
||||
// avoid duplicating the output produced by the input
|
||||
io.stdout = (buffer) =>
|
||||
length++ > input.length ? handlePoints(buffer) : 0;
|
||||
|
||||
interpreter.replInit();
|
||||
|
||||
// loop forever waiting for user inputs
|
||||
(function repl() {
|
||||
const out = decoder.decode(new Uint8Array(acc.splice(0)));
|
||||
// print in current line only the last line produced by the REPL
|
||||
const data = `${pyterminal_read(out.split("\n").at(-1))}\r`;
|
||||
length = 0;
|
||||
input = encoder.encode(data);
|
||||
for (const c of input) interpreter.replProcessChar(c);
|
||||
repl();
|
||||
})();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
interpreter.setStdout(generic);
|
||||
interpreter.setStderr(generic);
|
||||
interpreter.setStdin({
|
||||
isatty: false,
|
||||
stdin: () => pyterminal_read(data),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const pyTerminal = async (element) => {
|
||||
// lazy load these only when a valid terminal is found
|
||||
const [{ Terminal }, { Readline }, { FitAddon }] = await Promise.all([
|
||||
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();
|
||||
@@ -74,51 +171,54 @@ const pyTerminal = async () => {
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(readline);
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
terminal.open(target);
|
||||
fitAddon.fit();
|
||||
terminal.focus();
|
||||
defineProperty(element, "terminal", { value: terminal });
|
||||
defineProperties(element, {
|
||||
terminal: { value: terminal },
|
||||
process: {
|
||||
value: async (code) => {
|
||||
// this loop is the only way I could find to actually simulate
|
||||
// the user input char after char in a way that works in both
|
||||
// MicroPython and Pyodide
|
||||
for (const line of code.split(/(?:\r|\n|\r\n)/)) {
|
||||
terminal.paste(`${line}\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")) {
|
||||
// when the remote thread onReady triggers:
|
||||
// setup the interpreter stdout and stderr
|
||||
const workerReady = ({ interpreter }, { sync }) => {
|
||||
sync.pyterminal_drop_hooks();
|
||||
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),
|
||||
});
|
||||
};
|
||||
|
||||
// add a hook on the main thread to setup all sync helpers
|
||||
// also bootstrapping the XTerm target on main
|
||||
// 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);
|
||||
// allow a worker to drop main thread hooks ASAP
|
||||
xworker.sync.pyterminal_drop_hooks = () => {
|
||||
hooks.worker.onReady.delete(workerReady);
|
||||
};
|
||||
});
|
||||
|
||||
// setup remote thread JS/Python code for whenever the
|
||||
@@ -127,26 +227,71 @@ const pyTerminal = async () => {
|
||||
} else {
|
||||
// in the main case, just bootstrap XTerm without
|
||||
// allowing any input as that's not possible / awkward
|
||||
hooks.main.onReady.add(function main({ io }) {
|
||||
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||
console.warn("py-terminal is read only on main thread");
|
||||
hooks.main.onReady.delete(main);
|
||||
init({
|
||||
|
||||
// on main, it's easy to trash and clean the current terminal
|
||||
globalThis.__py_terminal__ = init({
|
||||
disableStdin: true,
|
||||
cursorBlink: false,
|
||||
cursorStyle: "underline",
|
||||
});
|
||||
io.stdout = (value) => {
|
||||
readline.write(`${value}\n`);
|
||||
};
|
||||
run("from js import __py_terminal__ as __terminal__");
|
||||
delete globalThis.__py_terminal__;
|
||||
|
||||
io.stderr = (error) => {
|
||||
readline.write(`${error.message || error}\n`);
|
||||
readline.write(String(error.message || error));
|
||||
};
|
||||
|
||||
if (type === "mpy") {
|
||||
interpreter.setStdin = Object; // as no-op
|
||||
interpreter.setStderr = Object; // as no-op
|
||||
interpreter.setStdout = ({ write }) => {
|
||||
io.stdout = write;
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const mo = new MutationObserver(pyTerminal);
|
||||
mo.observe(document, { childList: true, subtree: true });
|
||||
for (const key of TYPES.keys()) {
|
||||
const selector = `script[type="${key}"][terminal],${key}-script[terminal]`;
|
||||
SELECTORS.push(selector);
|
||||
customObserver.set(selector, async (element) => {
|
||||
// we currently support only one terminal on main as in "classic"
|
||||
const terminals = document.querySelectorAll(SELECTORS.join(","));
|
||||
if ([].filter.call(terminals, onceOnMain).length > 1)
|
||||
notifyAndThrow("You can use at most 1 main terminal");
|
||||
|
||||
// try to check the current document ASAP
|
||||
export default pyTerminal();
|
||||
// import styles lazily
|
||||
if (addStyle) {
|
||||
addStyle = false;
|
||||
document.head.append(
|
||||
Object.assign(document.createElement("link"), {
|
||||
rel: "stylesheet",
|
||||
href: new URL("./xterm.css", import.meta.url),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await pyTerminal(element);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,27 @@
|
||||
|
||||
import pyscript from "./stdlib/pyscript.js";
|
||||
|
||||
class Ignore extends Array {
|
||||
#add = false;
|
||||
#paths;
|
||||
#array;
|
||||
constructor(array, ...paths) {
|
||||
super();
|
||||
this.#array = array;
|
||||
this.#paths = paths;
|
||||
}
|
||||
push(...values) {
|
||||
if (this.#add) super.push(...values);
|
||||
return this.#array.push(...values);
|
||||
}
|
||||
path(path) {
|
||||
for (const _path of this.#paths) {
|
||||
// bails out at the first `true` value
|
||||
if ((this.#add = path.startsWith(_path))) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { entries } = Object;
|
||||
|
||||
const python = [
|
||||
@@ -16,16 +37,19 @@ const python = [
|
||||
"_path = None",
|
||||
];
|
||||
|
||||
const ignore = new Ignore(python, "./pyweb");
|
||||
|
||||
const write = (base, 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") {
|
||||
const code = JSON.stringify(value);
|
||||
python.push(`_path.write_text(${code})`);
|
||||
ignore.push(`_path.write_text(${code},encoding="utf-8")`);
|
||||
} else {
|
||||
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
|
||||
python.push(`if not _os.path.exists("${base}/${key}"):`);
|
||||
python.push(" _path.mkdir(parents=True, exist_ok=True)");
|
||||
ignore.push(`if not _os.path.exists("${base}/${key}"):`);
|
||||
ignore.push(" _path.mkdir(parents=True, exist_ok=True)");
|
||||
write(`${base}/${key}`, value);
|
||||
}
|
||||
}
|
||||
@@ -42,4 +66,5 @@ python.push(
|
||||
);
|
||||
python.push("\n");
|
||||
|
||||
export default python.join("\n");
|
||||
export const stdlib = python.join("\n");
|
||||
export const optional = ignore.join("\n");
|
||||
|
||||
@@ -30,19 +30,24 @@
|
||||
# as it works transparently in both the main thread and worker cases.
|
||||
|
||||
from pyscript.display import HTML, display
|
||||
from pyscript.fetch import fetch
|
||||
from pyscript.magic_js import (
|
||||
RUNNING_IN_WORKER,
|
||||
PyWorker,
|
||||
config,
|
||||
current_target,
|
||||
document,
|
||||
js_modules,
|
||||
sync,
|
||||
window,
|
||||
)
|
||||
from pyscript.websocket import WebSocket
|
||||
|
||||
try:
|
||||
from pyscript.event_handling import when
|
||||
except:
|
||||
# TODO: should we remove this? Or at the very least, we should capture
|
||||
# the traceback otherwise it's very hard to debug
|
||||
from pyscript.util import NotSupported
|
||||
|
||||
when = NotSupported(
|
||||
|
||||
@@ -6,17 +6,17 @@ import re
|
||||
from pyscript.magic_js import current_target, document, window
|
||||
|
||||
_MIME_METHODS = {
|
||||
"__repr__": "text/plain",
|
||||
"_repr_html_": "text/html",
|
||||
"_repr_markdown_": "text/markdown",
|
||||
"_repr_svg_": "image/svg+xml",
|
||||
"_repr_pdf_": "application/pdf",
|
||||
"_repr_jpeg_": "image/jpeg",
|
||||
"_repr_png_": "image/png",
|
||||
"_repr_latex": "text/latex",
|
||||
"_repr_json_": "application/json",
|
||||
"_repr_javascript_": "application/javascript",
|
||||
"savefig": "image/png",
|
||||
"_repr_javascript_": "application/javascript",
|
||||
"_repr_json_": "application/json",
|
||||
"_repr_latex": "text/latex",
|
||||
"_repr_png_": "image/png",
|
||||
"_repr_jpeg_": "image/jpeg",
|
||||
"_repr_pdf_": "application/pdf",
|
||||
"_repr_svg_": "image/svg+xml",
|
||||
"_repr_markdown_": "text/markdown",
|
||||
"_repr_html_": "text/html",
|
||||
"__repr__": "text/plain",
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ def _format_mime(obj):
|
||||
format_dict = mimebundle
|
||||
|
||||
output, not_available = None, []
|
||||
for method, mime_type in reversed(_MIME_METHODS.items()):
|
||||
for method, mime_type in _MIME_METHODS.items():
|
||||
if mime_type in format_dict:
|
||||
output = format_dict[mime_type]
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import inspect
|
||||
|
||||
from pyodide.ffi.wrappers import add_event_listener
|
||||
try:
|
||||
from pyodide.ffi.wrappers import add_event_listener
|
||||
|
||||
except ImportError:
|
||||
|
||||
def add_event_listener(el, event_type, func):
|
||||
el.addEventListener(event_type, func)
|
||||
|
||||
|
||||
from pyscript.magic_js import document
|
||||
|
||||
|
||||
@@ -27,7 +35,7 @@ def when(event_type=None, selector=None):
|
||||
f"Invalid selector: {selector}. Selector must"
|
||||
" be a string, a pydom.Element or a pydom.ElementCollection."
|
||||
)
|
||||
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
# Function doesn't receive events
|
||||
if not sig.parameters:
|
||||
@@ -35,11 +43,24 @@ def when(event_type=None, selector=None):
|
||||
def wrapper(*args, **kwargs):
|
||||
func()
|
||||
|
||||
else:
|
||||
wrapper = func
|
||||
|
||||
except AttributeError:
|
||||
# TODO: this is currently an quick hack to get micropython working but we need
|
||||
# to actually properly replace inspect.signature with something else
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except TypeError as e:
|
||||
if "takes 0 positional arguments" in str(e):
|
||||
return func()
|
||||
|
||||
raise
|
||||
|
||||
for el in elements:
|
||||
add_event_listener(el, event_type, wrapper)
|
||||
else:
|
||||
for el in elements:
|
||||
add_event_listener(el, event_type, func)
|
||||
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
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,14 +1,18 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
import js as globalThis
|
||||
from polyscript import config as _config
|
||||
from polyscript import js_modules
|
||||
from pyscript.util import NotSupported
|
||||
|
||||
RUNNING_IN_WORKER = not hasattr(globalThis, "document")
|
||||
|
||||
config = json.loads(globalThis.JSON.stringify(_config))
|
||||
|
||||
|
||||
# allow `from pyscript.js_modules.xxx import yyy`
|
||||
class JSModule(object):
|
||||
class JSModule:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
@@ -24,16 +28,33 @@ for name in globalThis.Reflect.ownKeys(js_modules):
|
||||
sys.modules["pyscript.js_modules"] = js_modules
|
||||
|
||||
if RUNNING_IN_WORKER:
|
||||
import js
|
||||
import polyscript
|
||||
|
||||
PyWorker = NotSupported(
|
||||
"pyscript.PyWorker",
|
||||
"pyscript.PyWorker works only when running in the main thread",
|
||||
)
|
||||
|
||||
try:
|
||||
globalThis.SharedArrayBuffer.new(4)
|
||||
import js
|
||||
|
||||
window = polyscript.xworker.window
|
||||
document = window.document
|
||||
js.document = document
|
||||
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
|
||||
|
||||
# in workers the display does not have a default ID
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import js
|
||||
|
||||
|
||||
def as_bytearray(buffer):
|
||||
ui8a = js.Uint8Array.new(buffer)
|
||||
size = ui8a.length
|
||||
ba = bytearray(size)
|
||||
for i in range(0, size):
|
||||
ba[i] = ui8a[i]
|
||||
return ba
|
||||
|
||||
|
||||
class NotSupported:
|
||||
"""
|
||||
Small helper that raises exceptions if you try to get/set any attribute on
|
||||
|
||||
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
pyscript.core/src/stdlib/pyweb/__init__.py
Normal file
1
pyscript.core/src/stdlib/pyweb/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .pydom import dom as pydom
|
||||
95
pyscript.core/src/stdlib/pyweb/media.py
Normal file
95
pyscript.core/src/stdlib/pyweb/media.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from pyodide.ffi import to_js
|
||||
from pyscript import window
|
||||
|
||||
|
||||
class Device:
|
||||
"""Device represents a media input or output device, such as a microphone,
|
||||
camera, or headset.
|
||||
"""
|
||||
|
||||
def __init__(self, device):
|
||||
self._js = device
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._js.deviceId
|
||||
|
||||
@property
|
||||
def group(self):
|
||||
return self._js.groupId
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
return self._js.kind
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self._js.label
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
@classmethod
|
||||
async def load(cls, audio=False, video=True):
|
||||
"""Load the device stream."""
|
||||
options = window.Object.new()
|
||||
options.audio = audio
|
||||
if isinstance(video, bool):
|
||||
options.video = video
|
||||
else:
|
||||
# TODO: Think this can be simplified but need to check it on the pyodide side
|
||||
|
||||
# TODO: this is pyodide specific. shouldn't be!
|
||||
options.video = window.Object.new()
|
||||
for k in video:
|
||||
setattr(
|
||||
options.video,
|
||||
k,
|
||||
to_js(video[k], dict_converter=window.Object.fromEntries),
|
||||
)
|
||||
|
||||
stream = await window.navigator.mediaDevices.getUserMedia(options)
|
||||
return stream
|
||||
|
||||
async def get_stream(self):
|
||||
key = self.kind.replace("input", "").replace("output", "")
|
||||
options = {key: {"deviceId": {"exact": self.id}}}
|
||||
|
||||
return await self.load(**options)
|
||||
|
||||
|
||||
async def list_devices() -> list[dict]:
|
||||
"""
|
||||
Return the list of the currently available media input and output devices,
|
||||
such as microphones, cameras, headsets, and so forth.
|
||||
|
||||
Output:
|
||||
|
||||
list(dict) - list of dictionaries representing the available media devices.
|
||||
Each dictionary has the following keys:
|
||||
* deviceId: a string that is an identifier for the represented device
|
||||
that is persisted across sessions. It is un-guessable by other
|
||||
applications and unique to the origin of the calling application.
|
||||
It is reset when the user clears cookies (for Private Browsing, a
|
||||
different identifier is used that is not persisted across sessions).
|
||||
|
||||
* groupId: a string that is a group identifier. Two devices have the same
|
||||
group identifier if they belong to the same physical device — for
|
||||
example a monitor with both a built-in camera and a microphone.
|
||||
|
||||
* kind: an enumerated value that is either "videoinput", "audioinput"
|
||||
or "audiooutput".
|
||||
|
||||
* label: a string describing this device (for example "External USB
|
||||
Webcam").
|
||||
|
||||
Note: the returned list will omit any devices that are blocked by the document
|
||||
Permission Policy: microphone, camera, speaker-selection (for output devices),
|
||||
and so on. Access to particular non-default devices is also gated by the
|
||||
Permissions API, and the list will omit devices for which the user has not
|
||||
granted explicit permission.
|
||||
"""
|
||||
# https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
|
||||
return [
|
||||
Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices()
|
||||
]
|
||||
@@ -1,9 +1,34 @@
|
||||
import sys
|
||||
import warnings
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
try:
|
||||
from typing import Any
|
||||
except ImportError:
|
||||
Any = "Any"
|
||||
|
||||
try:
|
||||
import warnings
|
||||
except ImportError:
|
||||
# TODO: For now it probably means we are in MicroPython. We should figure
|
||||
# out the "right" way to handle this. For now we just ignore the warning
|
||||
# and logging to console
|
||||
class warnings:
|
||||
@staticmethod
|
||||
def warn(*args, **kwargs):
|
||||
print("WARNING: ", *args, **kwargs)
|
||||
|
||||
|
||||
try:
|
||||
from functools import cached_property
|
||||
except ImportError:
|
||||
# TODO: same comment about micropython as above
|
||||
cached_property = property
|
||||
|
||||
try:
|
||||
from pyodide.ffi import JsProxy
|
||||
except ImportError:
|
||||
# TODO: same comment about micropython as above
|
||||
def JsProxy(obj):
|
||||
return obj
|
||||
|
||||
|
||||
from pyodide.ffi import JsProxy
|
||||
from pyscript import display, document, window
|
||||
|
||||
alert = window.alert
|
||||
@@ -100,6 +125,14 @@ class Element(BaseElement):
|
||||
def html(self, value):
|
||||
self._js.innerHTML = value
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._js.textContent
|
||||
|
||||
@text.setter
|
||||
def text(self, value):
|
||||
self._js.textContent = value
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
# TODO: This breaks with with standard template elements. Define how to best
|
||||
@@ -204,6 +237,91 @@ class Element(BaseElement):
|
||||
def show_me(self):
|
||||
self._js.scrollIntoView()
|
||||
|
||||
def snap(
|
||||
self,
|
||||
to: BaseElement | str = None,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
):
|
||||
"""
|
||||
Captures a snapshot of a video element. (Only available for video elements)
|
||||
|
||||
Inputs:
|
||||
|
||||
* to: element where to save the snapshot of the video frame to
|
||||
* width: width of the image
|
||||
* height: height of the image
|
||||
|
||||
Output:
|
||||
(Element) canvas element where the video frame snapshot was drawn into
|
||||
"""
|
||||
if self._js.tagName != "VIDEO":
|
||||
raise AttributeError("Snap method is only available for video Elements")
|
||||
|
||||
if to is None:
|
||||
canvas = self.create("canvas")
|
||||
if width is None:
|
||||
width = self._js.width
|
||||
if height is None:
|
||||
height = self._js.height
|
||||
canvas._js.width = width
|
||||
canvas._js.height = height
|
||||
|
||||
elif isistance(to, Element):
|
||||
if to._js.tagName != "CANVAS":
|
||||
raise TypeError("Element to snap to must a canvas.")
|
||||
canvas = to
|
||||
elif getattr(to, "tagName", "") == "CANVAS":
|
||||
canvas = Element(to)
|
||||
elif isinstance(to, str):
|
||||
canvas = pydom[to][0]
|
||||
if canvas._js.tagName != "CANVAS":
|
||||
raise TypeError("Element to snap to must a be canvas.")
|
||||
|
||||
canvas.draw(self, width, height)
|
||||
|
||||
return canvas
|
||||
|
||||
def download(self, filename: str = "snapped.png") -> None:
|
||||
"""Download the current element (only available for canvas elements) with the filename
|
||||
provided in input.
|
||||
|
||||
Inputs:
|
||||
* filename (str): name of the file being downloaded
|
||||
|
||||
Output:
|
||||
None
|
||||
"""
|
||||
if self._js.tagName != "CANVAS":
|
||||
raise AttributeError(
|
||||
"The download method is only available for canvas Elements"
|
||||
)
|
||||
|
||||
link = self.create("a")
|
||||
link._js.download = filename
|
||||
link._js.href = self._js.toDataURL()
|
||||
link._js.click()
|
||||
|
||||
def draw(self, what, width, height):
|
||||
"""Draw `what` on the current element (only available for canvas elements).
|
||||
|
||||
Inputs:
|
||||
|
||||
* what (canvas image source): An element to draw into the context. The specification permits any canvas
|
||||
image source, specifically, an HTMLImageElement, an SVGImageElement, an HTMLVideoElement,
|
||||
an HTMLCanvasElement, an ImageBitmap, an OffscreenCanvas, or a VideoFrame.
|
||||
"""
|
||||
if self._js.tagName != "CANVAS":
|
||||
raise AttributeError(
|
||||
"The draw method is only available for canvas Elements"
|
||||
)
|
||||
|
||||
if isinstance(what, Element):
|
||||
what = what._js
|
||||
|
||||
# https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
|
||||
self._js.getContext("2d").drawImage(what, 0, 0, width, height)
|
||||
|
||||
|
||||
class OptionsProxy:
|
||||
"""This class represents the options of a select element. It
|
||||
@@ -276,7 +394,7 @@ class OptionsProxy:
|
||||
return self.options[key]
|
||||
|
||||
|
||||
class StyleProxy(dict):
|
||||
class StyleProxy: # (dict):
|
||||
def __init__(self, element: Element) -> None:
|
||||
self._element = element
|
||||
|
||||
@@ -395,7 +513,7 @@ class ElementCollection:
|
||||
|
||||
|
||||
class DomScope:
|
||||
def __getattr__(self, __name: str) -> Any:
|
||||
def __getattr__(self, __name: str):
|
||||
element = document[f"#{__name}"]
|
||||
if element:
|
||||
return element[0]
|
||||
@@ -409,7 +527,12 @@ class PyDom(BaseElement):
|
||||
ElementCollection = ElementCollection
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(document)
|
||||
# PyDom is a special case of BaseElement where we don't want to create a new JS element
|
||||
# and it really doesn't have a need for styleproxy or parent to to call to __init__
|
||||
# (which actually fails in MP for some reason)
|
||||
self._js = document
|
||||
self._parent = None
|
||||
self._proxies = {}
|
||||
self.ids = DomScope()
|
||||
self.body = Element(document.body)
|
||||
self.head = Element(document.head)
|
||||
@@ -418,10 +541,6 @@ class PyDom(BaseElement):
|
||||
return super().create(type_, is_child=False, classes=classes, html=html)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, int):
|
||||
indices = range(*key.indices(len(self.list)))
|
||||
return [self.list[i] for i in indices]
|
||||
|
||||
elements = self._js.querySelectorAll(key)
|
||||
if not elements:
|
||||
return None
|
||||
@@ -429,5 +548,3 @@ class PyDom(BaseElement):
|
||||
|
||||
|
||||
dom = PyDom()
|
||||
|
||||
sys.modules[__name__] = dom
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export default {
|
||||
// allow pyterminal checks to bootstrap
|
||||
is_pyterminal: () => false,
|
||||
|
||||
/**
|
||||
* 'Sleep' for the given number of seconds. Used to implement Python's time.sleep in Worker threads.
|
||||
* @param {number} seconds The number of seconds to sleep.
|
||||
|
||||
24
pyscript.core/test/camera.html
Normal file
24
pyscript.core/test/camera.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript Media Example</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" src="camera.py" async></script>
|
||||
|
||||
<label for="cars">Choose a device:</label>
|
||||
|
||||
<select name="devices" id="devices"></select>
|
||||
|
||||
<button id="pick-device">Select the device</button>
|
||||
<button id="snap">Snap</button>
|
||||
|
||||
<div id="result"></div>
|
||||
|
||||
<video id="video" width="600" height="400" autoplay></video>
|
||||
</body>
|
||||
</html>
|
||||
32
pyscript.core/test/camera.py
Normal file
32
pyscript.core/test/camera.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from pyodide.ffi import create_proxy
|
||||
from pyscript import display, document, when, window
|
||||
from pyweb import media, pydom
|
||||
|
||||
devicesSelect = pydom["#devices"][0]
|
||||
video = pydom["video"][0]
|
||||
devices = {}
|
||||
|
||||
|
||||
async def list_media_devices(event=None):
|
||||
"""List the available media devices."""
|
||||
global devices
|
||||
for i, device in enumerate(await media.list_devices()):
|
||||
devices[device.id] = device
|
||||
label = f"{i} - ({device.kind}) {device.label} [{device.id}]"
|
||||
devicesSelect.options.add(value=device.id, html=label)
|
||||
|
||||
|
||||
@when("click", "#pick-device")
|
||||
async def connect_to_device(e):
|
||||
"""Connect to the selected device."""
|
||||
device = devices[devicesSelect.value]
|
||||
video._js.srcObject = await device.get_stream()
|
||||
|
||||
|
||||
@when("click", "#snap")
|
||||
async def camera_click(e):
|
||||
"""Take a picture and download it."""
|
||||
video.snap().download()
|
||||
|
||||
|
||||
await list_media_devices()
|
||||
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>
|
||||
@@ -6,6 +6,20 @@
|
||||
<title>PyScript Next Plugin</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
<py-config src="bad.toml" type="toml"></py-config>
|
||||
<mpy-config src="config-url/config.json"></mpy-config>
|
||||
<script type="mpy">
|
||||
from pyscript import config
|
||||
if config["files"]["{TO}"] != "./runtime":
|
||||
raise Exception("wrong config tree")
|
||||
|
||||
from runtime import test
|
||||
</script>
|
||||
<script type="mpy" worker>
|
||||
from pyscript import config
|
||||
if config["files"]["{TO}"] != "./runtime":
|
||||
raise Exception("wrong config tree")
|
||||
|
||||
from runtime import test
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
|
||||
7
pyscript.core/test/config-url/config.json
Normal file
7
pyscript.core/test/config-url/config.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files":{
|
||||
"{FROM}": "./src",
|
||||
"{TO}": "./runtime",
|
||||
"{FROM}/test.py": "{TO}/test.py"
|
||||
}
|
||||
}
|
||||
8
pyscript.core/test/config-url/src/test.py
Normal file
8
pyscript.core/test/config-url/src/test.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from pyscript import RUNNING_IN_WORKER, document
|
||||
|
||||
classList = document.documentElement.classList
|
||||
|
||||
if RUNNING_IN_WORKER:
|
||||
classList.add("worker")
|
||||
else:
|
||||
classList.add("main")
|
||||
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>
|
||||
@@ -51,3 +51,40 @@ test('MicroPython + Pyodide js_modules', async ({ page }) => {
|
||||
await expect(logs[3]).toBe(logs[4]);
|
||||
await expect(logs[4]).toBe(logs[5]);
|
||||
});
|
||||
|
||||
test('MicroPython + configURL', async ({ page }) => {
|
||||
const logs = [];
|
||||
page.on('console', msg => {
|
||||
const text = msg.text();
|
||||
if (!text.startsWith('['))
|
||||
logs.push(text);
|
||||
});
|
||||
await page.goto('http://localhost:8080/test/config-url.html');
|
||||
await page.waitForSelector('html.main.worker');
|
||||
});
|
||||
|
||||
test('Pyodide + terminal on Main', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/test/py-terminal-main.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
|
||||
test('Pyodide + terminal on Worker', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/test/py-terminal-worker.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
test('Pyodide + multiple terminals via Worker', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/test/py-terminals.html');
|
||||
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');
|
||||
});
|
||||
|
||||
31
pyscript.core/test/multiple-editors.html
Normal file
31
pyscript.core/test/multiple-editors.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>PyScript Test</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py-editor">
|
||||
0
|
||||
</script>
|
||||
<script type="py-editor">
|
||||
1
|
||||
</script>
|
||||
<script type="py-editor">
|
||||
2
|
||||
</script>
|
||||
<script type="py-editor">
|
||||
3
|
||||
</script>
|
||||
<script type="py-editor">
|
||||
4
|
||||
</script>
|
||||
<script type="py-editor">
|
||||
5
|
||||
</script>
|
||||
<!-- more... -->
|
||||
</body>
|
||||
</html>
|
||||
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"
|
||||
41
pyscript.core/test/py-editor/index.html
Normal file
41
pyscript.core/test/py-editor/index.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!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>
|
||||
<!-- 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>
|
||||
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
|
||||
14
pyscript.core/test/py-terminal-main.html
Normal file
14
pyscript.core/test/py-terminal-main.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PyTerminal Main</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
<style>.xterm { padding: .5rem; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<py-script src="terminal.py" terminal></py-script>
|
||||
</body>
|
||||
</html>
|
||||
15
pyscript.core/test/py-terminal-worker.html
Normal file
15
pyscript.core/test/py-terminal-worker.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PyTerminal Main</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
<style>.xterm { padding: .5rem; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" src="terminal.py" worker terminal></script>
|
||||
<script type="py" src="terminal.py" worker terminal></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,21 +9,10 @@
|
||||
<style>.xterm { padding: .5rem; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py">
|
||||
def greetings(event):
|
||||
print('hello world')
|
||||
<script type="mpy" worker terminal>
|
||||
print("µpython")
|
||||
import code
|
||||
code.interact()
|
||||
</script>
|
||||
<py-script worker 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>
|
||||
</html>
|
||||
|
||||
27
pyscript.core/test/py-terminals.html
Normal file
27
pyscript.core/test/py-terminals.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" />
|
||||
<title>PyTerminal Main</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
<style>.xterm { padding: .5rem; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" worker terminal>
|
||||
from pyscript import document
|
||||
document.documentElement.classList.add("first")
|
||||
|
||||
import code
|
||||
code.interact()
|
||||
</script>
|
||||
<script type="py" worker terminal>
|
||||
from pyscript import document
|
||||
document.documentElement.classList.add("second")
|
||||
|
||||
import code
|
||||
code.interact()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript Next Plugin</title>
|
||||
<title>PyDom Example</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime as dt
|
||||
|
||||
from pyscript import display
|
||||
from pyscript import display, when
|
||||
from pyweb import pydom
|
||||
from pyweb.base import when
|
||||
|
||||
|
||||
@when("click", "#just-a-button")
|
||||
def on_click(event):
|
||||
print(f"Hello from Python! {dt.now()}")
|
||||
display(f"Hello from Python! {dt.now()}", append=False, target="result")
|
||||
def on_click():
|
||||
try:
|
||||
timenow = dt.now()
|
||||
except NotImplementedError:
|
||||
# In this case we assume it's not implemented because we are using MycroPython
|
||||
tnow = time.localtime()
|
||||
tstr = "{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}"
|
||||
timenow = tstr.format(tnow[2], tnow[1], tnow[0], *tnow[2:])
|
||||
|
||||
display(f"Hello from PyScript, time is: {timenow}", append=False, target="result")
|
||||
|
||||
|
||||
@when("click", "#color-button")
|
||||
def on_color_click(event):
|
||||
print("1")
|
||||
btn = pydom["#result"]
|
||||
print("2")
|
||||
btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}"
|
||||
|
||||
|
||||
def reset_color():
|
||||
@when("click", "#color-reset-button")
|
||||
def reset_color(*args, **kwargs):
|
||||
pydom["#result"].style["background-color"] = "white"
|
||||
|
||||
|
||||
|
||||
19
pyscript.core/test/pydom_mp.html
Normal file
19
pyscript.core/test/pydom_mp.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">
|
||||
<title>PyDom Example (MicroPython)</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" src="pydom.py"></script>
|
||||
|
||||
<button id="just-a-button">Click For Time</button>
|
||||
<button id="color-button">Click For Color</button>
|
||||
<button id="color-reset-button">Reset Color</button>
|
||||
|
||||
<div id="result"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>PyperCard PyTest Suite</title>
|
||||
<title>PyDom Test Suite</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
@@ -32,7 +32,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" src="run_tests.py" config="tests.toml"></script>
|
||||
<script type="py" src="./run_tests.py" config="./tests.toml"></script>
|
||||
|
||||
<h1>pyscript.dom Tests</h1>
|
||||
<p>You can pass test parameters to this test suite by passing them as query params on the url.
|
||||
@@ -98,6 +98,8 @@
|
||||
<p class="collection"></p>
|
||||
<div class="collection"></div>
|
||||
<h3 class="collection"></h3>
|
||||
|
||||
<div id="element_attribute_tests"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -163,6 +163,30 @@ class TestElement:
|
||||
|
||||
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:
|
||||
def test_iter_eq_children(self):
|
||||
@@ -336,7 +360,7 @@ class TestSelect:
|
||||
assert select.options[0].html == "Option 1"
|
||||
|
||||
# WHEN we add another option (blank this time)
|
||||
select.options.add()
|
||||
select.options.add("")
|
||||
|
||||
# EXPECT the select element to have 2 options
|
||||
assert len(select.options) == 2
|
||||
|
||||
8
pyscript.core/test/terminal.py
Normal file
8
pyscript.core/test/terminal.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from pyscript import document
|
||||
|
||||
classList = document.documentElement.classList
|
||||
|
||||
if not __terminal__:
|
||||
classList.add("error")
|
||||
else:
|
||||
classList.add("ok")
|
||||
0
pyscript.core/test/test.html
Normal file
0
pyscript.core/test/test.html
Normal file
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -17,6 +17,7 @@ from playwright.sync_api import Error as PlaywrightError
|
||||
|
||||
ROOT = py.path.local(__file__).dirpath("..", "..", "..")
|
||||
BUILD = ROOT.join("pyscript.core").join("dist")
|
||||
TEST = ROOT.join("pyscript.core").join("test")
|
||||
|
||||
|
||||
def params_with_marks(params):
|
||||
@@ -206,6 +207,14 @@ class PyScriptTest:
|
||||
self.tmpdir = tmpdir
|
||||
# create a symlink to BUILD inside tmpdir
|
||||
tmpdir.join("build").mksymlinkto(BUILD)
|
||||
# create a symlink ALSO to dist folder so we can run the tests in
|
||||
# the test folder
|
||||
tmpdir.join("dist").mksymlinkto(BUILD)
|
||||
# create a symlink to TEST inside tmpdir so we can run tests in that
|
||||
# manual test folder
|
||||
tmpdir.join("test").mksymlinkto(TEST)
|
||||
|
||||
# create a symlink to the favicon, so that we can use it in the HTML
|
||||
self.tmpdir.chdir()
|
||||
self.tmpdir.join("favicon.ico").write("")
|
||||
self.logger = logger
|
||||
|
||||
30
pyscript.core/tests/integration/test_integration.py
Normal file
30
pyscript.core/tests/integration/test_integration.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from .support import PyScriptTest, with_execution_thread
|
||||
|
||||
|
||||
@with_execution_thread(None)
|
||||
class TestSmokeTests(PyScriptTest):
|
||||
"""
|
||||
Each example requires the same three tests:
|
||||
|
||||
- Test that the initial markup loads properly (currently done by
|
||||
testing the <title> tag's content)
|
||||
- Testing that pyscript is loading properly
|
||||
- Testing that the page contains appropriate content after rendering
|
||||
"""
|
||||
|
||||
def test_pydom(self):
|
||||
# Test the full pydom test suite by running it in the browser
|
||||
self.goto("test/pyscript_dom/index.html?-v&-s")
|
||||
assert self.page.title() == "PyDom Test Suite"
|
||||
|
||||
# wait for the test suite to finish
|
||||
self.wait_for_console(
|
||||
"============================= test session starts =============================="
|
||||
)
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
results = self.page.inner_html("#tests-terminal")
|
||||
assert results
|
||||
assert "PASSED" in results
|
||||
assert "FAILED" not in results
|
||||
@@ -7,6 +7,7 @@ from .support import PageErrors, PyScriptTest, only_worker, skip_worker
|
||||
|
||||
|
||||
class TestPyTerminal(PyScriptTest):
|
||||
@skip_worker("We do support multiple worker terminal now")
|
||||
def test_multiple_terminals(self):
|
||||
"""
|
||||
Multiple terminals are not currently supported
|
||||
@@ -19,9 +20,9 @@ class TestPyTerminal(PyScriptTest):
|
||||
wait_for_pyscript=False,
|
||||
check_js_errors=False,
|
||||
)
|
||||
assert self.assert_banner_message("You can use at most 1 terminal")
|
||||
assert self.assert_banner_message("You can use at most 1 main terminal")
|
||||
|
||||
with pytest.raises(PageErrors, match="You can use at most 1 terminal"):
|
||||
with pytest.raises(PageErrors, match="You can use at most 1 main terminal"):
|
||||
self.check_js_errors()
|
||||
|
||||
# TODO: interactive shell still unclear
|
||||
|
||||
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 };
|
||||
26
pyscript.core/types/core.d.ts
vendored
26
pyscript.core/types/core.d.ts
vendored
@@ -1,16 +1,31 @@
|
||||
export function offline_interpreter(config: any): string;
|
||||
import { stdlib } from "./stdlib.js";
|
||||
import { optional } from "./stdlib.js";
|
||||
import TYPES from "./types.js";
|
||||
/**
|
||||
* 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>}}
|
||||
* @returns {Promise<Worker & {sync: object}>}
|
||||
*/
|
||||
declare function exportedPyWorker(file: string, options?: {
|
||||
config?: string | object;
|
||||
async?: boolean;
|
||||
}): Worker & {
|
||||
sync: ProxyHandler<object>;
|
||||
};
|
||||
}): Promise<Worker & {
|
||||
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: {
|
||||
main: {
|
||||
onWorker: Set<Function>;
|
||||
@@ -38,5 +53,4 @@ declare const exportedHooks: {
|
||||
};
|
||||
declare const exportedConfig: {};
|
||||
declare const exportedWhenDefined: (type: string) => Promise<any>;
|
||||
import sync from "./sync.js";
|
||||
export { TYPES, exportedPyWorker as PyWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
||||
export { stdlib, optional, TYPES, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, 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>}
|
||||
*/
|
||||
export function robustFetch(url: string, options?: Request): Promise<Response>;
|
||||
export { getText };
|
||||
import { getText } from "polyscript/exports";
|
||||
export function getText(response: Response): Promise<string>;
|
||||
|
||||
2
pyscript.core/types/hooks.d.ts
vendored
2
pyscript.core/types/hooks.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
export function main(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 namespace hooks {
|
||||
namespace main {
|
||||
|
||||
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 default _default;
|
||||
export {};
|
||||
|
||||
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 default _default;
|
||||
export const stdlib: string;
|
||||
export const optional: string;
|
||||
|
||||
5
pyscript.core/types/stdlib/pyscript.d.ts
vendored
5
pyscript.core/types/stdlib/pyscript.d.ts
vendored
@@ -3,10 +3,15 @@ declare namespace _default {
|
||||
"__init__.py": string;
|
||||
"display.py": string;
|
||||
"event_handling.py": string;
|
||||
"fetch.py": string;
|
||||
"ffi.py": string;
|
||||
"magic_js.py": string;
|
||||
"util.py": string;
|
||||
"websocket.py": string;
|
||||
};
|
||||
let pyweb: {
|
||||
"__init__.py": string;
|
||||
"media.py": string;
|
||||
"pydom.py": string;
|
||||
};
|
||||
}
|
||||
|
||||
1
pyscript.core/types/sync.d.ts
vendored
1
pyscript.core/types/sync.d.ts
vendored
@@ -1,4 +1,5 @@
|
||||
declare namespace _default {
|
||||
function is_pyterminal(): boolean;
|
||||
/**
|
||||
* 'Sleep' for the given number of seconds. Used to implement Python's time.sleep in Worker threads.
|
||||
* @param {number} seconds The number of seconds to sleep.
|
||||
|
||||
Reference in New Issue
Block a user