mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4e25d879e | ||
|
|
c82dbb755e | ||
|
|
1ed77321a5 | ||
|
|
e36a57eb06 | ||
|
|
ee3cd76022 | ||
|
|
eb31e51a45 | ||
|
|
c8c2dd0806 | ||
|
|
e525d54be0 | ||
|
|
7b9f7c13f5 | ||
|
|
7582cbef9c | ||
|
|
b395cde49c | ||
|
|
9f46234f71 | ||
|
|
f4c4edeb29 | ||
|
|
7166c32384 | ||
|
|
ed126889ae | ||
|
|
0d0ea96435 | ||
|
|
fafdf74007 | ||
|
|
999897df12 | ||
|
|
d47fb58ede | ||
|
|
f316341e73 | ||
|
|
8c46fcabf7 | ||
|
|
e4ff4d8fab | ||
|
|
f20a0003ed | ||
|
|
6c938dfe3b | ||
|
|
d884586a82 | ||
|
|
f8f7ba89c1 | ||
|
|
67d47511d5 | ||
|
|
6f49f18937 | ||
|
|
7b8ef7ebe2 | ||
|
|
461ae38763 | ||
|
|
4b90ebdef5 | ||
|
|
15c19aa708 | ||
|
|
d0406be84c | ||
|
|
aab015b9b8 | ||
|
|
a1e5a05b49 | ||
|
|
f1a787e031 | ||
|
|
b41cfb7b60 | ||
|
|
1c675307e1 | ||
|
|
ac56f82c6d | ||
|
|
2ac5ca79d7 | ||
|
|
cb9ee6f7e2 | ||
|
|
9abaef33bd | ||
|
|
320a537db2 | ||
|
|
9b775ce015 | ||
|
|
66f72eda1e | ||
|
|
39ca29749c | ||
|
|
85da548447 | ||
|
|
9985787e4b | ||
|
|
18ec6ce775 | ||
|
|
ed6d0136b8 | ||
|
|
e7216d26e7 | ||
|
|
d1a0d8ea98 | ||
|
|
04222b0d03 | ||
|
|
8ec3381789 | ||
|
|
9bd4737708 | ||
|
|
c49cb9231b | ||
|
|
d1d1c5740f | ||
|
|
1a05ea5fd2 | ||
|
|
5b4e8527da |
17
.github/workflows/prepare-release.yml
vendored
17
.github/workflows/prepare-release.yml
vendored
@@ -19,7 +19,22 @@ jobs:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Python venv
|
||||
run: python -m venv env
|
||||
|
||||
- name: Activate Python
|
||||
run: source env/bin/activate
|
||||
|
||||
- name: Update pip
|
||||
run: pip install --upgrade pip
|
||||
|
||||
- name: Install PyMinifier
|
||||
run: pip install --ignore-requires-python python-minifier
|
||||
|
||||
- name: Install Setuptools
|
||||
run: pip install setuptools
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
|
||||
21
.github/workflows/publish-release.yml
vendored
21
.github/workflows/publish-release.yml
vendored
@@ -21,7 +21,22 @@ jobs:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Python venv
|
||||
run: python -m venv env
|
||||
|
||||
- name: Activate Python
|
||||
run: source env/bin/activate
|
||||
|
||||
- name: Update pip
|
||||
run: pip install --upgrade pip
|
||||
|
||||
- name: Install PyMinifier
|
||||
run: pip install --ignore-requires-python python-minifier
|
||||
|
||||
- name: Install Setuptools
|
||||
run: pip install setuptools
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
@@ -46,6 +61,10 @@ jobs:
|
||||
working-directory: .
|
||||
run: sed 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' ./public/index.html > ./pyscript.core/dist/index.html
|
||||
|
||||
- name: Generate release.tar from snapshot and put it in dist/
|
||||
working-directory: .
|
||||
run: tar -cvf ../release.tar * && mv ../release.tar .
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
|
||||
17
.github/workflows/publish-snapshot.yml
vendored
17
.github/workflows/publish-snapshot.yml
vendored
@@ -25,7 +25,22 @@ jobs:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Python venv
|
||||
run: python -m venv env
|
||||
|
||||
- name: Activate Python
|
||||
run: source env/bin/activate
|
||||
|
||||
- name: Update pip
|
||||
run: pip install --upgrade pip
|
||||
|
||||
- name: Install PyMinifier
|
||||
run: pip install --ignore-requires-python python-minifier
|
||||
|
||||
- name: Install Setuptools
|
||||
run: pip install setuptools
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
|
||||
17
.github/workflows/publish-unstable.yml
vendored
17
.github/workflows/publish-unstable.yml
vendored
@@ -26,7 +26,22 @@ jobs:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Python venv
|
||||
run: python -m venv env
|
||||
|
||||
- name: Activate Python
|
||||
run: source env/bin/activate
|
||||
|
||||
- name: Update pip
|
||||
run: pip install --upgrade pip
|
||||
|
||||
- name: Install PyMinifier
|
||||
run: pip install --ignore-requires-python python-minifier
|
||||
|
||||
- name: Install Setuptools
|
||||
run: pip install setuptools
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -142,6 +142,7 @@ coverage/
|
||||
test_results
|
||||
|
||||
# @pyscript/core npm artifacts
|
||||
pyscript.core/test-results/*
|
||||
pyscript.core/core.*
|
||||
pyscript.core/dist
|
||||
pyscript.core/dist.zip
|
||||
|
||||
@@ -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,13 +25,13 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.3.0
|
||||
rev: 24.8.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell # See 'pyproject.toml' for args
|
||||
exclude: \.js\.map$
|
||||
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# Release Notes
|
||||
|
||||
## 2024.05.21
|
||||
|
||||
### Features
|
||||
|
||||
### Bug fixes
|
||||
|
||||
### Enhancements
|
||||
|
||||
- `py-editor` run buttons now display a spinner when disabled, which occurs when the editor is running code.
|
||||
|
||||
## 2023.05.01
|
||||
|
||||
### Features
|
||||
|
||||
@@ -59,9 +59,9 @@ If you would like to contribute to PyScript, but you aren't sure where to begin,
|
||||
|
||||
## Setting up your local environment and developing
|
||||
|
||||
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://pyscript.github.io/docs/latest/development/setting-up-environment.html) will help you get started.
|
||||
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://docs.pyscript.net/latest/contributing/#set-up-your-development-environment) will help you get started.
|
||||
|
||||
You can also read about PyScript's [development process](https://pyscript.github.io/docs/latest/development/developing.html) to learn how to contribute code to PyScript, how to run tests and what's the PR etiquette of the community!
|
||||
You can also read about PyScript's [development process](https://docs.pyscript.net/latest/developers/) to learn how to contribute code to PyScript, how to run tests and what's the PR etiquette of the community!
|
||||
|
||||
## License terms for contributions
|
||||
|
||||
|
||||
27
README.md
27
README.md
@@ -38,11 +38,11 @@ To try PyScript, import the appropriate pyscript files into the `<head>` tag of
|
||||
<head>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://pyscript.net/releases/2023.11.2/core.css"
|
||||
href="https://pyscript.net/releases/2024.6.2/core.css"
|
||||
/>
|
||||
<script
|
||||
type="module"
|
||||
src="https://pyscript.net/releases/2023.11.2/core.js"
|
||||
src="https://pyscript.net/releases/2024.6.2/core.js"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -67,10 +67,29 @@ Check out the [official docs](https://docs.pyscript.net/) for more detailed docu
|
||||
|
||||
## How to Contribute
|
||||
|
||||
Read the [contributing guide](CONTRIBUTING.md) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
|
||||
Read the [contributing guide](https://docs.pyscript.net/latest/contributing/) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
|
||||
|
||||
Check out the [developing process](https://pyscript.github.io/docs/latest/contributing) documentation for more information on how to setup your development environment.
|
||||
Check out the [developing process](https://docs.pyscript.net/latest/developers/) documentation for more information on how to setup your development environment.
|
||||
|
||||
## Governance
|
||||
|
||||
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.
|
||||
|
||||
## Release
|
||||
|
||||
To cut a new release of PyScript simply
|
||||
[add a new release](https://github.com/pyscript/pyscript/releases) while
|
||||
remembering to write a comprehensive changelog. A [GitHub action](https://github.com/pyscript/pyscript/blob/main/.github/workflows/publish-release.yml)
|
||||
will kick in and ensure the release is described and deployed to a URL with the
|
||||
pattern: https://pyscript.net/releases/YYYY.M.v/ (year/month/version - as per
|
||||
our [CalVer](https://calver.org/) versioning scheme).
|
||||
|
||||
Then, the following three separate repositories need updating:
|
||||
|
||||
- [Documentation](https://github.com/pyscript/docs) - Change the `version.json`
|
||||
file in the root of the directory and then `node version-update.js`.
|
||||
- [Homepage](https://github.com/pyscript/pyscript.net) - Ensure the version
|
||||
referenced in `index.html` is the latest version.
|
||||
- [PSDC](https://pyscript.com) - Use discord or Anaconda Slack (if you work at
|
||||
Anaconda) to let the PSDC team know there's a new version, so they can update
|
||||
their project templates.
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: "eslint:recommended",
|
||||
overrides: [
|
||||
{
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
files: [".eslintrc.{js,cjs}"],
|
||||
parserOptions: {
|
||||
sourceType: "script",
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
ignorePatterns: ["3rd-party"],
|
||||
rules: {
|
||||
"no-implicit-globals": ["error"],
|
||||
},
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
.eslintrc.cjs
|
||||
.pytest_cache/
|
||||
node_modules/
|
||||
rollup/
|
||||
test/
|
||||
tests/
|
||||
src/stdlib/_pyscript
|
||||
src/stdlib/pyscript.py
|
||||
package-lock.json
|
||||
tsconfig.json
|
||||
@@ -12,7 +12,7 @@ Clone this repository then run `npm install` within its folder.
|
||||
|
||||
Use `npm run build` to create all artifacts and _dist_ files.
|
||||
|
||||
Use `npm run server` to test locally, via the `http://localhost:8080/test/` url, smoke tests or to test manually anything you'd like to check.
|
||||
Use `npm run server` to test locally, via the `http://localhost:8080/tests/` url, smoke tests or to test manually anything you'd like to check.
|
||||
|
||||
### Artifacts
|
||||
|
||||
|
||||
22
pyscript.core/eslint.config.mjs
Normal file
22
pyscript.core/eslint.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import globals from "globals";
|
||||
import js from "@eslint/js";
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
{
|
||||
ignores: ["**/3rd-party/"],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.es2021,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-implicit-globals": ["error"],
|
||||
},
|
||||
},
|
||||
];
|
||||
1921
pyscript.core/package-lock.json
generated
1921
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.4.22",
|
||||
"version": "0.5.12",
|
||||
"type": "module",
|
||||
"description": "PyScript",
|
||||
"module": "./index.js",
|
||||
@@ -8,6 +8,15 @@
|
||||
"jsdelivr": "./jsdelivr.js",
|
||||
"browser": "./index.js",
|
||||
"main": "./index.js",
|
||||
"files": [
|
||||
"./dist/",
|
||||
"./src/",
|
||||
"./types/",
|
||||
"./index.js",
|
||||
"./jsdelivr.js",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/core.d.ts",
|
||||
@@ -16,17 +25,23 @@
|
||||
"./css": {
|
||||
"import": "./dist/core.css"
|
||||
},
|
||||
"./storage": {
|
||||
"import": "./dist/storage.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"server": "npx static-handler --coi .",
|
||||
"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",
|
||||
"server": "echo \"➡️ TESTS @ $(tput bold)http://localhost:8080/tests/$(tput sgr0)\"; npx static-handler --coi .",
|
||||
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && npm run build:tests-index && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run ts && npm run test:integration; fi",
|
||||
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
||||
"build:flatted": "node rollup/flatted.cjs",
|
||||
"build:plugins": "node rollup/plugins.cjs",
|
||||
"build:stdlib": "node rollup/stdlib.cjs",
|
||||
"build:3rd-party": "node rollup/3rd-party.cjs",
|
||||
"build:tests-index": "node rollup/build_test_index.cjs",
|
||||
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||
"test: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:integration": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel tests/integration.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||
"test:ws": "bun tests/ws/index.js & playwright test tests/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",
|
||||
@@ -41,33 +56,37 @@
|
||||
"license": "APACHE-2.0",
|
||||
"dependencies": {
|
||||
"@ungap/with-resolvers": "^0.1.0",
|
||||
"@webreflection/idb-map": "^0.3.1",
|
||||
"basic-devtools": "^0.1.6",
|
||||
"polyscript": "^0.12.6",
|
||||
"polyscript": "^0.15.6",
|
||||
"sabayon": "^0.5.2",
|
||||
"sticky-module": "^0.1.1",
|
||||
"to-json-callback": "^0.1.1",
|
||||
"type-checked-collections": "^0.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.5.0",
|
||||
"@codemirror/lang-python": "^6.1.5",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/commands": "^6.6.1",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/language": "^6.10.2",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.26.3",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@codemirror/view": "^6.33.0",
|
||||
"@playwright/test": "1.45.3",
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@webreflection/toml-j0.4": "^1.1.3",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"bun": "^1.1.27",
|
||||
"chokidar": "^4.0.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"eslint": "^9.1.1",
|
||||
"rollup": "^4.16.4",
|
||||
"eslint": "^9.10.0",
|
||||
"flatted": "^3.3.1",
|
||||
"rollup": "^4.21.3",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"static-handler": "^0.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"static-handler": "^0.5.3",
|
||||
"typescript": "^5.6.2",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-readline": "^1.1.1"
|
||||
},
|
||||
|
||||
73
pyscript.core/rollup/build_test_index.cjs
Normal file
73
pyscript.core/rollup/build_test_index.cjs
Normal file
@@ -0,0 +1,73 @@
|
||||
const { join } = require("node:path");
|
||||
const { lstatSync, readdirSync, writeFileSync } = require("node:fs");
|
||||
|
||||
// folders to not consider while crawling
|
||||
const EXCLUDE_DIR = new Set(["ws"]);
|
||||
|
||||
const TEST_DIR = join(__dirname, "..", "tests");
|
||||
|
||||
const TEST_INDEX = join(TEST_DIR, "index.html");
|
||||
|
||||
const crawl = (path, tree = {}) => {
|
||||
for (const file of readdirSync(path)) {
|
||||
const current = join(path, file);
|
||||
if (current === TEST_INDEX) continue;
|
||||
if (lstatSync(current).isDirectory()) {
|
||||
if (EXCLUDE_DIR.has(file)) continue;
|
||||
const sub = {};
|
||||
tree[file] = sub;
|
||||
crawl(current, sub);
|
||||
if (!Reflect.ownKeys(sub).length) {
|
||||
delete tree[file];
|
||||
}
|
||||
} else if (file.endsWith(".html")) {
|
||||
const name = file === "index.html" ? "." : file.slice(0, -5);
|
||||
tree[name] = current.replace(TEST_DIR, "");
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
};
|
||||
|
||||
const createList = (tree) => {
|
||||
const ul = ["<ul>"];
|
||||
for (const [key, value] of Object.entries(tree)) {
|
||||
ul.push("<li>");
|
||||
if (typeof value === "string") {
|
||||
ul.push(`<a href=".${value}">${key}<small>.html</small></a>`);
|
||||
} else {
|
||||
if ("." in value) {
|
||||
ul.push(`<strong><a href=".${value["."]}">${key}</a></strong>`);
|
||||
delete value["."];
|
||||
} else {
|
||||
ul.push(`<strong><span>${key}</span></strong>`);
|
||||
}
|
||||
if (Reflect.ownKeys(value).length) ul.push(createList(value));
|
||||
}
|
||||
ul.push("</li>");
|
||||
}
|
||||
ul.push("</ul>");
|
||||
return ul.join("");
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
TEST_INDEX,
|
||||
`<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript tests</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; }
|
||||
a {
|
||||
display: block;
|
||||
transition: opacity .3s;
|
||||
}
|
||||
a, span { opacity: .7; }
|
||||
a:hover { opacity: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>${createList(crawl(TEST_DIR))}</body>
|
||||
</html>
|
||||
`,
|
||||
);
|
||||
@@ -40,4 +40,17 @@ export default [
|
||||
warn(warning);
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "./src/storage.js",
|
||||
plugins: plugins.concat(
|
||||
process.env.NO_MIN
|
||||
? [nodeResolve(), commonjs()]
|
||||
: [nodeResolve(), commonjs(), terser()],
|
||||
),
|
||||
output: {
|
||||
esModule: true,
|
||||
dir: "./dist",
|
||||
sourcemap: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
17
pyscript.core/rollup/flatted.cjs
Normal file
17
pyscript.core/rollup/flatted.cjs
Normal file
@@ -0,0 +1,17 @@
|
||||
const { writeFileSync, readFileSync } = require("node:fs");
|
||||
const { join } = require("node:path");
|
||||
|
||||
const flatted = "# https://www.npmjs.com/package/flatted\n\n";
|
||||
const source = join(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"flatted",
|
||||
"python",
|
||||
"flatted.py",
|
||||
);
|
||||
const dest = join(__dirname, "..", "src", "stdlib", "pyscript", "flatted.py");
|
||||
|
||||
const clear = (str) => String(str).replace(/^#.*/gm, "").trimStart();
|
||||
|
||||
writeFileSync(dest, flatted + clear(readFileSync(source)));
|
||||
@@ -4,13 +4,27 @@ const {
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} = require("node:fs");
|
||||
|
||||
const { spawnSync } = require("node:child_process");
|
||||
|
||||
const { join } = require("node:path");
|
||||
|
||||
const crawl = (path, json) => {
|
||||
for (const file of readdirSync(path)) {
|
||||
const full = join(path, file);
|
||||
if (/\.py$/.test(file)) json[file] = readFileSync(full).toString();
|
||||
else if (statSync(full).isDirectory() && !file.endsWith("_"))
|
||||
if (/\.py$/.test(file)) {
|
||||
if (process.env.NO_MIN) json[file] = readFileSync(full).toString();
|
||||
else {
|
||||
const {
|
||||
output: [error, result],
|
||||
} = spawnSync("pyminify", [
|
||||
"--remove-literal-statements",
|
||||
full,
|
||||
]);
|
||||
if (error) process.exit(1);
|
||||
json[file] = result.toString();
|
||||
}
|
||||
} else if (statSync(full).isDirectory() && !file.endsWith("_"))
|
||||
crawl(full, (json[file] = {}));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,6 +45,8 @@ const configDetails = async (config, type) => {
|
||||
|
||||
const conflictError = (reason) => new Error(`(${CONFLICTING_CODE}): ${reason}`);
|
||||
|
||||
const relative_url = (url, base = location.href) => new URL(url, base).href;
|
||||
|
||||
const syntaxError = (type, url, { message }) => {
|
||||
let str = `(${BAD_CONFIG}): Invalid ${type}`;
|
||||
if (url) str += ` @ ${url}`;
|
||||
@@ -108,7 +110,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;
|
||||
if (url) configURL = relative_url(url);
|
||||
config = text;
|
||||
if (json || type === "json") {
|
||||
try {
|
||||
@@ -153,4 +155,4 @@ for (const [TYPE] of TYPES) {
|
||||
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||
}
|
||||
|
||||
export default configs;
|
||||
export { configs, relative_url };
|
||||
|
||||
@@ -42,3 +42,34 @@ mpy-config {
|
||||
.mpy-editor-run-button:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.py-editor-run-button:disabled > *,
|
||||
.mpy-editor-run-button:disabled > * {
|
||||
display: none; /* hide all the child elements of the run button when it is disabled */
|
||||
}
|
||||
.py-editor-run-button:disabled,
|
||||
.mpy-editor-run-button:disabled {
|
||||
border-width: 0;
|
||||
}
|
||||
.py-editor-run-button:disabled::before,
|
||||
.mpy-editor-run-button:disabled::before {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 100%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -23px; /* hardcoded value to center the spinner on the run button */
|
||||
margin-left: -26px; /* hardcoded value to center the spinner on the run button */
|
||||
border-radius: 50%;
|
||||
border: 2px solid #aaa;
|
||||
border-top-color: #000;
|
||||
background-color: #fff;
|
||||
animation: spinner 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
define,
|
||||
defineProperty,
|
||||
dispatch,
|
||||
isSync,
|
||||
queryTarget,
|
||||
unescape,
|
||||
whenDefined,
|
||||
@@ -19,15 +20,22 @@ import {
|
||||
|
||||
import "./all-done.js";
|
||||
import TYPES from "./types.js";
|
||||
import configs from "./config.js";
|
||||
import { configs, relative_url } from "./config.js";
|
||||
import sync from "./sync.js";
|
||||
import bootstrapNodeAndPlugins from "./plugins-helper.js";
|
||||
import { ErrorCode } from "./exceptions.js";
|
||||
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||
import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
|
||||
import {
|
||||
hooks,
|
||||
main,
|
||||
worker,
|
||||
codeFor,
|
||||
createFunction,
|
||||
inputFailure,
|
||||
} from "./hooks.js";
|
||||
|
||||
import { stdlib, optional } from "./stdlib.js";
|
||||
export { stdlib, optional };
|
||||
export { stdlib, optional, inputFailure };
|
||||
|
||||
// generic helper to disambiguate between custom element and script
|
||||
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
||||
@@ -77,6 +85,7 @@ const [
|
||||
|
||||
export {
|
||||
TYPES,
|
||||
relative_url,
|
||||
exportedPyWorker as PyWorker,
|
||||
exportedMPWorker as MPWorker,
|
||||
exportedHooks as hooks,
|
||||
@@ -84,6 +93,9 @@ export {
|
||||
exportedWhenDefined as whenDefined,
|
||||
};
|
||||
|
||||
export const offline_interpreter = (config) =>
|
||||
config?.interpreter && relative_url(config.interpreter);
|
||||
|
||||
const hooked = new Map();
|
||||
|
||||
for (const [TYPE, interpreter] of TYPES) {
|
||||
@@ -147,6 +159,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
// enrich the Python env with some JS utility for main
|
||||
interpreter.registerJsModule("_pyscript", {
|
||||
PyWorker,
|
||||
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
||||
get target() {
|
||||
return isScript(currentElement)
|
||||
? currentElement.target.id
|
||||
@@ -190,15 +203,13 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
}
|
||||
|
||||
if (isScript(element)) {
|
||||
const {
|
||||
attributes: { async: isAsync, target },
|
||||
} = element;
|
||||
const hasTarget = !!target?.value;
|
||||
const show = hasTarget
|
||||
? queryTarget(element, target.value)
|
||||
const isAsync = !isSync(element);
|
||||
const target = element.getAttribute("target");
|
||||
const show = target
|
||||
? queryTarget(element, target)
|
||||
: document.createElement("script-py");
|
||||
|
||||
if (!hasTarget) {
|
||||
if (!target) {
|
||||
const { head, body } = document;
|
||||
if (head.contains(element)) body.append(show);
|
||||
else element.after(show);
|
||||
@@ -294,7 +305,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
interpreter,
|
||||
hooks,
|
||||
env: `${TYPE}-script`,
|
||||
version: config?.interpreter,
|
||||
version: offline_interpreter(config),
|
||||
onerror(error, element) {
|
||||
errors.set(element, error);
|
||||
},
|
||||
@@ -319,7 +330,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
async connectedCallback() {
|
||||
if (!this.executed) {
|
||||
this.executed = true;
|
||||
const isAsync = this.hasAttribute("async");
|
||||
const isAsync = !isSync(this);
|
||||
const { io, run, runAsync } = await this._wrap
|
||||
.promise;
|
||||
this.srcCode = await fetchSource(
|
||||
|
||||
@@ -46,7 +46,7 @@ export const createFunction = (self, name) => {
|
||||
const SetFunction = typedSet({ typeof: "function" });
|
||||
const SetString = typedSet({ typeof: "string" });
|
||||
|
||||
const inputFailure = `
|
||||
export const inputFailure = `
|
||||
import builtins
|
||||
def input(prompt=""):
|
||||
raise Exception("\\n ".join([
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// PyScript py-editor plugin
|
||||
import { Hook, XWorker, dedent } from "polyscript/exports";
|
||||
import { TYPES, stdlib } from "../core.js";
|
||||
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
|
||||
import { notify } from "./error.js";
|
||||
|
||||
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
||||
|
||||
@@ -23,6 +24,11 @@ const hooks = {
|
||||
},
|
||||
};
|
||||
|
||||
const validate = (config, result) => {
|
||||
if (typeof result === "boolean") throw `Invalid source: ${config}`;
|
||||
return result;
|
||||
};
|
||||
|
||||
async function execute({ currentTarget }) {
|
||||
const { env, pySrc, outDiv } = this;
|
||||
const hasRunButton = !!currentTarget;
|
||||
@@ -34,14 +40,39 @@ async function execute({ currentTarget }) {
|
||||
|
||||
if (!envs.has(env)) {
|
||||
const srcLink = URL.createObjectURL(new Blob([""]));
|
||||
const details = { type: this.interpreter };
|
||||
const details = {
|
||||
type: this.interpreter,
|
||||
serviceWorker: this.serviceWorker,
|
||||
};
|
||||
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()));
|
||||
// verify that config can be parsed and used
|
||||
try {
|
||||
details.configURL = relative_url(config);
|
||||
if (config.endsWith(".toml")) {
|
||||
const [{ parse }, toml] = await Promise.all([
|
||||
import(
|
||||
/* webpackIgnore: true */ "../3rd-party/toml.js"
|
||||
),
|
||||
fetch(config).then((r) => r.ok && r.text()),
|
||||
]);
|
||||
details.config = parse(validate(config, toml));
|
||||
} else if (config.endsWith(".json")) {
|
||||
const json = await fetch(config).then(
|
||||
(r) => r.ok && r.json(),
|
||||
);
|
||||
details.config = validate(config, json);
|
||||
} else {
|
||||
details.configURL = relative_url("./config.txt");
|
||||
details.config = JSON.parse(config);
|
||||
}
|
||||
details.version = offline_interpreter(details.config);
|
||||
} catch (error) {
|
||||
notify(error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
details.config = {};
|
||||
}
|
||||
|
||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||
@@ -57,12 +88,15 @@ async function execute({ currentTarget }) {
|
||||
|
||||
// wait for the env then set the target div
|
||||
// before executing the current code
|
||||
envs.get(env).then((xworker) => {
|
||||
return envs.get(env).then((xworker) => {
|
||||
xworker.onerror = ({ error }) => {
|
||||
if (hasRunButton) {
|
||||
outDiv.innerHTML += `<span style='color:red'>${
|
||||
outDiv.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<span style='color:red'>${
|
||||
error.message || error
|
||||
}</span>\n`;
|
||||
}</span>\n`,
|
||||
);
|
||||
}
|
||||
console.error(error);
|
||||
};
|
||||
@@ -73,31 +107,41 @@ async function execute({ currentTarget }) {
|
||||
const { sync } = xworker;
|
||||
sync.write = (str) => {
|
||||
if (hasRunButton) outDiv.innerText += `${str}\n`;
|
||||
else console.log(str);
|
||||
};
|
||||
sync.writeErr = (str) => {
|
||||
if (hasRunButton) {
|
||||
outDiv.innerHTML += `<span style='color:red'>${str}</span>\n`;
|
||||
outDiv.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<span style='color:red'>${str}</span>\n`,
|
||||
);
|
||||
} else {
|
||||
notify(str);
|
||||
console.error(str);
|
||||
}
|
||||
};
|
||||
sync.runAsync(pySrc).then(enable, enable);
|
||||
});
|
||||
}
|
||||
|
||||
const makeRunButton = (listener, type) => {
|
||||
const makeRunButton = (handler, type) => {
|
||||
const runButton = document.createElement("button");
|
||||
runButton.className = `absolute ${type}-editor-run-button`;
|
||||
runButton.innerHTML = RUN_BUTTON;
|
||||
runButton.setAttribute("aria-label", "Python Script Run Button");
|
||||
runButton.addEventListener("click", listener);
|
||||
runButton.addEventListener("click", async (event) => {
|
||||
runButton.blur();
|
||||
await handler.handleEvent(event);
|
||||
});
|
||||
return runButton;
|
||||
};
|
||||
|
||||
const makeEditorDiv = (listener, type) => {
|
||||
const makeEditorDiv = (handler, type) => {
|
||||
const editorDiv = document.createElement("div");
|
||||
editorDiv.className = `${type}-editor-input`;
|
||||
editorDiv.setAttribute("aria-label", "Python Script Area");
|
||||
|
||||
const runButton = makeRunButton(listener, type);
|
||||
const runButton = makeRunButton(handler, type);
|
||||
const editorShadowContainer = document.createElement("div");
|
||||
|
||||
// avoid outer elements intercepting key events (reveal as example)
|
||||
@@ -117,15 +161,15 @@ const makeOutDiv = (type) => {
|
||||
return outDiv;
|
||||
};
|
||||
|
||||
const makeBoxDiv = (listener, type) => {
|
||||
const makeBoxDiv = (handler, type) => {
|
||||
const boxDiv = document.createElement("div");
|
||||
boxDiv.className = `${type}-editor-box`;
|
||||
|
||||
const editorDiv = makeEditorDiv(listener, type);
|
||||
const editorDiv = makeEditorDiv(handler, type);
|
||||
const outDiv = makeOutDiv(type);
|
||||
boxDiv.append(editorDiv, outDiv);
|
||||
|
||||
return [boxDiv, outDiv];
|
||||
return [boxDiv, outDiv, editorDiv.querySelector("button")];
|
||||
};
|
||||
|
||||
const init = async (script, type, interpreter) => {
|
||||
@@ -135,7 +179,7 @@ const init = async (script, type, interpreter) => {
|
||||
{ python },
|
||||
{ indentUnit },
|
||||
{ keymap },
|
||||
{ defaultKeymap },
|
||||
{ defaultKeymap, indentWithTab },
|
||||
] = await Promise.all([
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
||||
@@ -147,10 +191,19 @@ const init = async (script, type, interpreter) => {
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
||||
]);
|
||||
|
||||
const isSetup = script.hasAttribute("setup");
|
||||
let isSetup = script.hasAttribute("setup");
|
||||
const hasConfig = script.hasAttribute("config");
|
||||
const serviceWorker = script.getAttribute("service-worker");
|
||||
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
|
||||
|
||||
// helps preventing too lazy ServiceWorker initialization on button run
|
||||
if (serviceWorker) {
|
||||
new XWorker("data:application/javascript,postMessage(0)", {
|
||||
type: "dummy",
|
||||
serviceWorker,
|
||||
}).onmessage = ({ target }) => target.terminate();
|
||||
}
|
||||
|
||||
if (hasConfig && configs.has(env)) {
|
||||
throw new SyntaxError(
|
||||
configs.get(env)
|
||||
@@ -161,15 +214,29 @@ const init = async (script, type, interpreter) => {
|
||||
|
||||
configs.set(env, hasConfig);
|
||||
|
||||
const source = script.src
|
||||
? await fetch(script.src).then((b) => b.text())
|
||||
: script.textContent;
|
||||
let source = script.textContent;
|
||||
|
||||
// verify the src points to a valid file that can be parsed
|
||||
const { src } = script;
|
||||
if (src) {
|
||||
try {
|
||||
source = validate(
|
||||
src,
|
||||
await fetch(src).then((b) => b.ok && b.text()),
|
||||
);
|
||||
} catch (error) {
|
||||
notify(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const context = {
|
||||
// allow the listener to be overridden at distance
|
||||
handleEvent: execute,
|
||||
serviceWorker,
|
||||
interpreter,
|
||||
env,
|
||||
config:
|
||||
hasConfig &&
|
||||
new URL(script.getAttribute("config"), location.href).href,
|
||||
config: hasConfig && script.getAttribute("config"),
|
||||
get pySrc() {
|
||||
return isSetup ? source : editor.state.doc.toString();
|
||||
},
|
||||
@@ -178,14 +245,82 @@ const init = async (script, type, interpreter) => {
|
||||
},
|
||||
};
|
||||
|
||||
let target;
|
||||
defineProperties(script, {
|
||||
target: { get: () => target },
|
||||
handleEvent: {
|
||||
get: () => context.handleEvent,
|
||||
set: (callback) => {
|
||||
// do not bother with logic if it was set back as its original handler
|
||||
if (callback === execute) context.handleEvent = execute;
|
||||
// in every other case be sure that if the listener override returned
|
||||
// `false` nothing happens, otherwise keep doing what it always did
|
||||
else {
|
||||
context.handleEvent = async (event) => {
|
||||
// trap the currentTarget ASAP (if any)
|
||||
// otherwise it gets lost asynchronously
|
||||
const { currentTarget } = event;
|
||||
// augment a code snapshot before invoking the override
|
||||
defineProperties(event, {
|
||||
code: { value: context.pySrc },
|
||||
});
|
||||
// avoid executing the default handler if the override returned `false`
|
||||
if ((await callback(event)) !== false)
|
||||
await execute.call(context, { currentTarget });
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
code: {
|
||||
get: () => context.pySrc,
|
||||
set: (insert) => {
|
||||
if (isSetup) return;
|
||||
editor.update([
|
||||
editor.state.update({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
insert,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
},
|
||||
process: {
|
||||
/**
|
||||
* Simulate a setup node overriding the source to evaluate.
|
||||
* @param {string} code the Python code to evaluate.
|
||||
* @returns {Promise<...>} fulfill once code has been evaluated.
|
||||
*/
|
||||
value(code) {
|
||||
const wasSetup = isSetup;
|
||||
const wasSource = source;
|
||||
isSetup = true;
|
||||
source = code;
|
||||
const restore = () => {
|
||||
isSetup = wasSetup;
|
||||
source = wasSource;
|
||||
};
|
||||
return context
|
||||
.handleEvent({ currentTarget: null })
|
||||
.then(restore, restore);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const notifyEditor = () => {
|
||||
const event = new Event(`${type}-editor`, { bubbles: true });
|
||||
script.dispatchEvent(event);
|
||||
};
|
||||
|
||||
if (isSetup) {
|
||||
execute.call(context, { currentTarget: null });
|
||||
await context.handleEvent({ currentTarget: null });
|
||||
notifyEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
const selector = script.getAttribute("target");
|
||||
|
||||
let target;
|
||||
if (selector) {
|
||||
target =
|
||||
document.getElementById(selector) ||
|
||||
@@ -202,8 +337,7 @@ const init = async (script, type, interpreter) => {
|
||||
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
|
||||
|
||||
// @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);
|
||||
const [boxDiv, outDiv, runButton] = makeBoxDiv(context, type);
|
||||
boxDiv.dataset.env = script.hasAttribute("env") ? env : interpreter;
|
||||
|
||||
const inputChild = boxDiv.querySelector(`.${type}-editor-input > div`);
|
||||
@@ -216,8 +350,9 @@ const init = async (script, type, interpreter) => {
|
||||
const doc = dedent(script.textContent).trim();
|
||||
|
||||
// preserve user indentation, if any
|
||||
const indentation = /^(\s+)/m.test(doc) ? RegExp.$1 : " ";
|
||||
const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
|
||||
|
||||
const listener = () => runButton.click();
|
||||
const editor = new EditorView({
|
||||
extensions: [
|
||||
indentUnit.of(indentation),
|
||||
@@ -227,14 +362,19 @@ const init = async (script, type, interpreter) => {
|
||||
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
|
||||
{ key: "Cmd-Enter", run: listener, preventDefault: true },
|
||||
{ key: "Shift-Enter", run: listener, preventDefault: true },
|
||||
// @see https://codemirror.net/examples/tab/
|
||||
indentWithTab,
|
||||
]),
|
||||
basicSetup,
|
||||
],
|
||||
foldGutter: true,
|
||||
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||
parent,
|
||||
doc,
|
||||
});
|
||||
|
||||
editor.focus();
|
||||
notifyEditor();
|
||||
};
|
||||
|
||||
// avoid too greedy MutationObserver operations at distance
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// PyScript py-terminal plugin
|
||||
import { TYPES, hooks } from "../core.js";
|
||||
import { TYPES, relative_url } from "../core.js";
|
||||
import { notify } from "./error.js";
|
||||
import { customObserver, defineProperties } from "polyscript/exports";
|
||||
import { customObserver } from "polyscript/exports";
|
||||
|
||||
// will contain all valid selectors
|
||||
const SELECTORS = [];
|
||||
|
||||
// avoid processing same elements twice
|
||||
const processed = new WeakSet();
|
||||
|
||||
// show the error on main and
|
||||
// stops the module from keep executing
|
||||
const notifyAndThrow = (message) => {
|
||||
@@ -15,265 +18,10 @@ const notifyAndThrow = (message) => {
|
||||
|
||||
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
||||
|
||||
const bootstrapped = new WeakSet();
|
||||
|
||||
let addStyle = true;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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 }, { WebLinksAddon }] =
|
||||
await Promise.all([
|
||||
import(/* webpackIgnore: true */ "../3rd-party/xterm.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/xterm-readline.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js"),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../3rd-party/xterm_addon-web-links.js"
|
||||
),
|
||||
]);
|
||||
|
||||
const readline = new Readline();
|
||||
|
||||
// common main thread initialization for both worker
|
||||
// or main case, bootstrapping the terminal on its target
|
||||
const init = (options) => {
|
||||
let target = element;
|
||||
const selector = element.getAttribute("target");
|
||||
if (selector) {
|
||||
target =
|
||||
document.getElementById(selector) ||
|
||||
document.querySelector(selector);
|
||||
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||
} else {
|
||||
target = document.createElement("py-terminal");
|
||||
target.style.display = "block";
|
||||
element.after(target);
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
theme: {
|
||||
background: "#191A19",
|
||||
foreground: "#F5F2E7",
|
||||
},
|
||||
...options,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(readline);
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
terminal.open(target);
|
||||
fitAddon.fit();
|
||||
terminal.focus();
|
||||
defineProperties(element, {
|
||||
terminal: { value: terminal },
|
||||
process: {
|
||||
value: async (code) => {
|
||||
// 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")) {
|
||||
// add a hook on the main thread to setup all sync helpers
|
||||
// also bootstrapping the XTerm target on main *BUT* ...
|
||||
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||
// ... as multiple workers will add multiple callbacks
|
||||
// be sure no xworker is ever initialized twice!
|
||||
if (bootstrapped.has(xworker)) return;
|
||||
bootstrapped.add(xworker);
|
||||
|
||||
// still cleanup this callback for future scripts/workers
|
||||
hooks.main.onWorker.delete(worker);
|
||||
|
||||
init({
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
});
|
||||
|
||||
xworker.sync.is_pyterminal = () => true;
|
||||
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
||||
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
||||
});
|
||||
|
||||
// setup remote thread JS/Python code for whenever the
|
||||
// worker is ready to become a terminal
|
||||
hooks.worker.onReady.add(workerReady);
|
||||
} else {
|
||||
// in the main case, just bootstrap XTerm without
|
||||
// allowing any input as that's not possible / awkward
|
||||
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||
console.warn("py-terminal is read only on main thread");
|
||||
hooks.main.onReady.delete(main);
|
||||
|
||||
// on main, it's easy to trash and clean the current terminal
|
||||
globalThis.__py_terminal__ = init({
|
||||
disableStdin: true,
|
||||
cursorBlink: false,
|
||||
cursorStyle: "underline",
|
||||
});
|
||||
run("from js import __py_terminal__ as __terminal__");
|
||||
delete globalThis.__py_terminal__;
|
||||
|
||||
io.stderr = (error) => {
|
||||
readline.write(String(error.message || error));
|
||||
};
|
||||
|
||||
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),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for (const key of TYPES.keys()) {
|
||||
const selector = `script[type="${key}"][terminal],${key}-script[terminal]`;
|
||||
for (const type of TYPES.keys()) {
|
||||
const selector = `script[type="${type}"][terminal],${type}-script[terminal]`;
|
||||
SELECTORS.push(selector);
|
||||
customObserver.set(selector, async (element) => {
|
||||
// we currently support only one terminal on main as in "classic"
|
||||
@@ -287,11 +35,26 @@ for (const key of TYPES.keys()) {
|
||||
document.head.append(
|
||||
Object.assign(document.createElement("link"), {
|
||||
rel: "stylesheet",
|
||||
href: new URL("./xterm.css", import.meta.url),
|
||||
href: relative_url("./xterm.css", import.meta.url),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await pyTerminal(element);
|
||||
if (processed.has(element)) return;
|
||||
processed.add(element);
|
||||
|
||||
const bootstrap = (module) => module.default(element);
|
||||
|
||||
// we can't be smart with template literals for the dynamic import
|
||||
// or bundlers are incapable of producing multiple files around
|
||||
if (type === "mpy") {
|
||||
await import(/* webpackIgnore: true */ "./py-terminal/mpy.js").then(
|
||||
bootstrap,
|
||||
);
|
||||
} else {
|
||||
await import(/* webpackIgnore: true */ "./py-terminal/py.js").then(
|
||||
bootstrap,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
252
pyscript.core/src/plugins/py-terminal/mpy.js
Normal file
252
pyscript.core/src/plugins/py-terminal/mpy.js
Normal file
@@ -0,0 +1,252 @@
|
||||
// PyScript pyodide terminal plugin
|
||||
import { hooks, inputFailure } from "../../core.js";
|
||||
import { defineProperties } from "polyscript/exports";
|
||||
|
||||
const bootstrapped = new WeakSet();
|
||||
|
||||
// this callback will be serialized as string and it never needs
|
||||
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||
// only once thanks to the `sync.is_pyterminal()` check.
|
||||
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||
if (type !== "mpy" || !sync.is_pyterminal()) return;
|
||||
|
||||
const { pyterminal_ready, pyterminal_read, pyterminal_write } = sync;
|
||||
|
||||
interpreter.registerJsModule("_pyscript_input", {
|
||||
input: pyterminal_read,
|
||||
});
|
||||
|
||||
run(
|
||||
[
|
||||
"from _pyscript_input import input",
|
||||
"from polyscript import currentScript as _",
|
||||
"__terminal__ = _.terminal",
|
||||
"del _",
|
||||
].join(";"),
|
||||
);
|
||||
|
||||
const missingReturn = new Uint8Array([13]);
|
||||
io.stdout = (buffer) => {
|
||||
if (buffer[0] === 10) pyterminal_write(missingReturn);
|
||||
pyterminal_write(buffer);
|
||||
};
|
||||
io.stderr = (error) => {
|
||||
pyterminal_write(String(error.message || error));
|
||||
};
|
||||
|
||||
// tiny shim of the code module with only interact
|
||||
// to bootstrap a REPL like environment
|
||||
interpreter.registerJsModule("code", {
|
||||
interact() {
|
||||
const encoder = new TextEncoderStream();
|
||||
encoder.readable.pipeTo(
|
||||
new WritableStream({
|
||||
write(buffer) {
|
||||
for (const c of buffer) interpreter.replProcessChar(c);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const writer = encoder.writable.getWriter();
|
||||
sync.pyterminal_stream_write = (buffer) => writer.write(buffer);
|
||||
|
||||
interpreter.replInit();
|
||||
},
|
||||
});
|
||||
|
||||
pyterminal_ready();
|
||||
};
|
||||
|
||||
export default async (element) => {
|
||||
// lazy load these only when a valid terminal is found
|
||||
const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
|
||||
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||
import(/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||
),
|
||||
]);
|
||||
|
||||
const terminalOptions = {
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
};
|
||||
|
||||
let stream;
|
||||
|
||||
// common main thread initialization for both worker
|
||||
// or main case, bootstrapping the terminal on its target
|
||||
const init = () => {
|
||||
let target = element;
|
||||
const selector = element.getAttribute("target");
|
||||
if (selector) {
|
||||
target =
|
||||
document.getElementById(selector) ||
|
||||
document.querySelector(selector);
|
||||
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||
} else {
|
||||
target = document.createElement("py-terminal");
|
||||
target.style.display = "block";
|
||||
element.after(target);
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
theme: {
|
||||
background: "#191A19",
|
||||
foreground: "#F5F2E7",
|
||||
},
|
||||
...terminalOptions,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
terminal.open(target);
|
||||
fitAddon.fit();
|
||||
terminal.focus();
|
||||
defineProperties(element, {
|
||||
terminal: { value: terminal },
|
||||
process: {
|
||||
value: async (code) => {
|
||||
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||
await stream.write(`${line}\r`);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
return terminal;
|
||||
};
|
||||
|
||||
// branch logic for the worker
|
||||
if (element.hasAttribute("worker")) {
|
||||
// add a hook on the main thread to setup all sync helpers
|
||||
// also bootstrapping the XTerm target on main *BUT* ...
|
||||
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||
// ... as multiple workers will add multiple callbacks
|
||||
// be sure no xworker is ever initialized twice!
|
||||
if (bootstrapped.has(xworker)) return;
|
||||
bootstrapped.add(xworker);
|
||||
|
||||
// still cleanup this callback for future scripts/workers
|
||||
hooks.main.onWorker.delete(worker);
|
||||
|
||||
const terminal = init();
|
||||
|
||||
const { sync } = xworker;
|
||||
|
||||
// handle the read mode on input
|
||||
let promisedChunks = null;
|
||||
let readChunks = "";
|
||||
|
||||
sync.is_pyterminal = () => true;
|
||||
|
||||
// put the terminal in a read-only state
|
||||
// frees the worker on \r
|
||||
sync.pyterminal_read = (buffer) => {
|
||||
terminal.write(buffer);
|
||||
promisedChunks = Promise.withResolvers();
|
||||
return promisedChunks.promise;
|
||||
};
|
||||
|
||||
// write if not reading input
|
||||
sync.pyterminal_write = (buffer) => {
|
||||
if (!promisedChunks) terminal.write(buffer);
|
||||
};
|
||||
|
||||
// add the onData terminal listener which forwards to the worker
|
||||
// everything typed in a queued char-by-char way
|
||||
sync.pyterminal_ready = () => {
|
||||
let queue = Promise.resolve();
|
||||
stream = {
|
||||
write: (buffer) =>
|
||||
(queue = queue.then(() =>
|
||||
sync.pyterminal_stream_write(buffer),
|
||||
)),
|
||||
};
|
||||
terminal.onData((buffer) => {
|
||||
if (promisedChunks) {
|
||||
// handle backspace on input
|
||||
if (buffer === "\x7f") {
|
||||
// avoid over-greedy backspace
|
||||
if (readChunks.length) {
|
||||
readChunks = readChunks.slice(0, -1);
|
||||
// override previous char position
|
||||
// put an empty space to clear the char
|
||||
// move back position again
|
||||
buffer = "\b \b";
|
||||
} else buffer = "";
|
||||
} else readChunks += buffer;
|
||||
if (buffer) {
|
||||
terminal.write(buffer);
|
||||
if (readChunks.endsWith("\r")) {
|
||||
terminal.write("\n");
|
||||
promisedChunks.resolve(readChunks.slice(0, -1));
|
||||
promisedChunks = null;
|
||||
readChunks = "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stream.write(buffer);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// setup remote thread JS/Python code for whenever the
|
||||
// worker is ready to become a terminal
|
||||
hooks.worker.onReady.add(workerReady);
|
||||
} else {
|
||||
// ⚠️ In an ideal world the inputFailure should never be used on main.
|
||||
// However, Pyodide still can't compete with MicroPython REPL mode
|
||||
// so while it's OK to keep that entry on main as default, we need
|
||||
// to remove it ASAP from `mpy` use cases, otherwise MicroPython would
|
||||
// also throw whenever an `input(...)` is required / digited.
|
||||
hooks.main.codeBeforeRun.delete(inputFailure);
|
||||
|
||||
// in the main case, just bootstrap XTerm without
|
||||
// allowing any input as that's not possible / awkward
|
||||
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||
if (type !== "mpy") return;
|
||||
|
||||
hooks.main.onReady.delete(main);
|
||||
|
||||
const terminal = init();
|
||||
|
||||
const missingReturn = new Uint8Array([13]);
|
||||
io.stdout = (buffer) => {
|
||||
if (buffer[0] === 10) terminal.write(missingReturn);
|
||||
terminal.write(buffer);
|
||||
};
|
||||
|
||||
// expose the __terminal__ one-off reference
|
||||
globalThis.__py_terminal__ = terminal;
|
||||
run(
|
||||
[
|
||||
"from js import prompt as input",
|
||||
"from js import __py_terminal__ as __terminal__",
|
||||
].join(";"),
|
||||
);
|
||||
delete globalThis.__py_terminal__;
|
||||
|
||||
// NOTE: this is NOT the same as the one within
|
||||
// the onWorkerReady callback!
|
||||
interpreter.registerJsModule("code", {
|
||||
interact() {
|
||||
const encoder = new TextEncoderStream();
|
||||
encoder.readable.pipeTo(
|
||||
new WritableStream({
|
||||
write(buffer) {
|
||||
for (const c of buffer)
|
||||
interpreter.replProcessChar(c);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
stream = encoder.writable.getWriter();
|
||||
terminal.onData((buffer) => stream.write(buffer));
|
||||
|
||||
interpreter.replInit();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
179
pyscript.core/src/plugins/py-terminal/py.js
Normal file
179
pyscript.core/src/plugins/py-terminal/py.js
Normal file
@@ -0,0 +1,179 @@
|
||||
// PyScript py-terminal plugin
|
||||
import { hooks } from "../../core.js";
|
||||
import { defineProperties } from "polyscript/exports";
|
||||
|
||||
const bootstrapped = new WeakSet();
|
||||
|
||||
// this callback will be serialized as string and it never needs
|
||||
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||
// only once thanks to the `sync.is_pyterminal()` check.
|
||||
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||
if (type !== "py" || !sync.is_pyterminal()) return;
|
||||
|
||||
run(
|
||||
[
|
||||
"from polyscript import currentScript as _",
|
||||
"__terminal__ = _.terminal",
|
||||
"del _",
|
||||
].join(";"),
|
||||
);
|
||||
|
||||
let data = "";
|
||||
const { pyterminal_read, pyterminal_write } = sync;
|
||||
const decoder = new TextDecoder();
|
||||
const generic = {
|
||||
isatty: false,
|
||||
write(buffer) {
|
||||
data = decoder.decode(buffer);
|
||||
pyterminal_write(data);
|
||||
return buffer.length;
|
||||
},
|
||||
};
|
||||
|
||||
io.stderr = (error) => {
|
||||
pyterminal_write(String(error.message || error));
|
||||
};
|
||||
|
||||
interpreter.setStdout(generic);
|
||||
interpreter.setStderr(generic);
|
||||
interpreter.setStdin({
|
||||
isatty: false,
|
||||
stdin: () => pyterminal_read(data),
|
||||
});
|
||||
};
|
||||
|
||||
export default async (element) => {
|
||||
// lazy load these only when a valid terminal is found
|
||||
const [{ Terminal }, { Readline }, { FitAddon }, { WebLinksAddon }] =
|
||||
await Promise.all([
|
||||
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../../3rd-party/xterm-readline.js"
|
||||
),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"
|
||||
),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||
),
|
||||
]);
|
||||
|
||||
const readline = new Readline();
|
||||
|
||||
// common main thread initialization for both worker
|
||||
// or main case, bootstrapping the terminal on its target
|
||||
const init = (options) => {
|
||||
let target = element;
|
||||
const selector = element.getAttribute("target");
|
||||
if (selector) {
|
||||
target =
|
||||
document.getElementById(selector) ||
|
||||
document.querySelector(selector);
|
||||
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||
} else {
|
||||
target = document.createElement("py-terminal");
|
||||
target.style.display = "block";
|
||||
element.after(target);
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
theme: {
|
||||
background: "#191A19",
|
||||
foreground: "#F5F2E7",
|
||||
},
|
||||
...options,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(readline);
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
terminal.open(target);
|
||||
fitAddon.fit();
|
||||
terminal.focus();
|
||||
defineProperties(element, {
|
||||
terminal: { value: terminal },
|
||||
process: {
|
||||
value: async (code) => {
|
||||
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||
terminal.paste(`${line}`);
|
||||
terminal.write("\r\n");
|
||||
do {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 0),
|
||||
);
|
||||
} while (!readline.activeRead?.resolve);
|
||||
readline.activeRead.resolve(line);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
return terminal;
|
||||
};
|
||||
|
||||
// branch logic for the worker
|
||||
if (element.hasAttribute("worker")) {
|
||||
// add a hook on the main thread to setup all sync helpers
|
||||
// also bootstrapping the XTerm target on main *BUT* ...
|
||||
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||
// ... as multiple workers will add multiple callbacks
|
||||
// be sure no xworker is ever initialized twice!
|
||||
if (bootstrapped.has(xworker)) return;
|
||||
bootstrapped.add(xworker);
|
||||
|
||||
// still cleanup this callback for future scripts/workers
|
||||
hooks.main.onWorker.delete(worker);
|
||||
|
||||
init({
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
});
|
||||
|
||||
xworker.sync.is_pyterminal = () => true;
|
||||
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
||||
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
||||
});
|
||||
|
||||
// setup remote thread JS/Python code for whenever the
|
||||
// worker is ready to become a terminal
|
||||
hooks.worker.onReady.add(workerReady);
|
||||
} else {
|
||||
// in the main case, just bootstrap XTerm without
|
||||
// allowing any input as that's not possible / awkward
|
||||
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||
if (type !== "py") return;
|
||||
|
||||
console.warn("py-terminal is read only on main thread");
|
||||
hooks.main.onReady.delete(main);
|
||||
|
||||
// on main, it's easy to trash and clean the current terminal
|
||||
globalThis.__py_terminal__ = init({
|
||||
disableStdin: true,
|
||||
cursorBlink: false,
|
||||
cursorStyle: "underline",
|
||||
});
|
||||
run("from js import __py_terminal__ as __terminal__");
|
||||
delete globalThis.__py_terminal__;
|
||||
|
||||
io.stderr = (error) => {
|
||||
readline.write(String(error.message || error));
|
||||
};
|
||||
|
||||
let data = "";
|
||||
const decoder = new TextDecoder();
|
||||
const generic = {
|
||||
isatty: false,
|
||||
write(buffer) {
|
||||
data = decoder.decode(buffer);
|
||||
readline.write(data);
|
||||
return buffer.length;
|
||||
},
|
||||
};
|
||||
interpreter.setStdout(generic);
|
||||
interpreter.setStderr(generic);
|
||||
interpreter.setStdin({
|
||||
isatty: false,
|
||||
stdin: () => readline.read(data),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -37,7 +37,7 @@ const python = [
|
||||
"_path = None",
|
||||
];
|
||||
|
||||
const ignore = new Ignore(python, "./pyweb");
|
||||
const ignore = new Ignore(python, "-");
|
||||
|
||||
const write = (base, literal) => {
|
||||
for (const [key, value] of entries(literal)) {
|
||||
|
||||
@@ -29,17 +29,25 @@
|
||||
# pyscript.magic_js. This is the blessed way to access them from pyscript,
|
||||
# as it works transparently in both the main thread and worker cases.
|
||||
|
||||
from polyscript import lazy_py_modules as py_import
|
||||
from pyscript.display import HTML, display
|
||||
from pyscript.fetch import fetch
|
||||
from pyscript.magic_js import (
|
||||
RUNNING_IN_WORKER,
|
||||
PyWorker,
|
||||
config,
|
||||
current_target,
|
||||
document,
|
||||
js_import,
|
||||
js_modules,
|
||||
sync,
|
||||
window,
|
||||
)
|
||||
from pyscript.storage import Storage, storage
|
||||
from pyscript.websocket import WebSocket
|
||||
|
||||
if not RUNNING_IN_WORKER:
|
||||
from pyscript.workers import create_named_worker, workers
|
||||
|
||||
try:
|
||||
from pyscript.event_handling import when
|
||||
|
||||
@@ -19,27 +19,36 @@ def when(event_type=None, selector=None):
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
|
||||
from pyscript.web import Element, ElementCollection
|
||||
|
||||
if isinstance(selector, str):
|
||||
elements = document.querySelectorAll(selector)
|
||||
else:
|
||||
# TODO: This is a hack that will be removed when pyscript becomes a package
|
||||
# and we can better manage the imports without circular dependencies
|
||||
from pyweb import pydom
|
||||
|
||||
if isinstance(selector, pydom.Element):
|
||||
elements = [selector._js]
|
||||
elif isinstance(selector, pydom.ElementCollection):
|
||||
elements = [el._js for el in selector]
|
||||
elif isinstance(selector, Element):
|
||||
elements = [selector._dom_element]
|
||||
elif isinstance(selector, ElementCollection):
|
||||
elements = [el._dom_element for el in selector]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid selector: {selector}. Selector must"
|
||||
" be a string, a pydom.Element or a pydom.ElementCollection."
|
||||
)
|
||||
if isinstance(selector, list):
|
||||
elements = selector
|
||||
else:
|
||||
elements = [selector]
|
||||
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
# Function doesn't receive events
|
||||
if not sig.parameters:
|
||||
|
||||
# Function is async: must be awaited
|
||||
if inspect.iscoroutinefunction(func):
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
await func()
|
||||
|
||||
else:
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
func()
|
||||
|
||||
@@ -47,13 +56,14 @@ def when(event_type=None, selector=None):
|
||||
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
|
||||
# TODO: this is very ugly hack to get micropython working because inspect.signature
|
||||
# doesn't exist, but we need to actually properly replace inspect.signature.
|
||||
# It may be actually better to not try any magic for now and raise the error
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except TypeError as e:
|
||||
if "takes 0 positional arguments" in str(e):
|
||||
if "takes" in str(e) and "positional arguments" in str(e):
|
||||
return func()
|
||||
|
||||
raise
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
|
||||
import js
|
||||
from pyscript.util import as_bytearray
|
||||
|
||||
|
||||
### wrap the response to grant Pythonic results
|
||||
@@ -12,14 +13,6 @@ class _Response:
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._response, attr)
|
||||
|
||||
def _as_bytearray(self, buffer):
|
||||
ui8a = js.Uint8Array.new(buffer)
|
||||
size = ui8a.length
|
||||
ba = bytearray(size)
|
||||
for i in range(0, size):
|
||||
ba[i] = ui8a[i]
|
||||
return ba
|
||||
|
||||
# exposed methods with Pythonic results
|
||||
async def arrayBuffer(self):
|
||||
buffer = await self._response.arrayBuffer()
|
||||
@@ -27,14 +20,14 @@ class _Response:
|
||||
if hasattr(buffer, "to_py"):
|
||||
return buffer.to_py()
|
||||
# shims in MicroPython
|
||||
return memoryview(self._as_bytearray(buffer))
|
||||
return memoryview(as_bytearray(buffer))
|
||||
|
||||
async def blob(self):
|
||||
return await self._response.blob()
|
||||
|
||||
async def bytearray(self):
|
||||
buffer = await self._response.arrayBuffer()
|
||||
return self._as_bytearray(buffer)
|
||||
return as_bytearray(buffer)
|
||||
|
||||
async def json(self):
|
||||
return json.loads(await self.text())
|
||||
|
||||
148
pyscript.core/src/stdlib/pyscript/flatted.py
Normal file
148
pyscript.core/src/stdlib/pyscript/flatted.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# https://www.npmjs.com/package/flatted
|
||||
|
||||
import json as _json
|
||||
|
||||
|
||||
class _Known:
|
||||
def __init__(self):
|
||||
self.key = []
|
||||
self.value = []
|
||||
|
||||
|
||||
class _String:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
|
||||
def _array_keys(value):
|
||||
keys = []
|
||||
i = 0
|
||||
for _ in value:
|
||||
keys.append(i)
|
||||
i += 1
|
||||
return keys
|
||||
|
||||
|
||||
def _object_keys(value):
|
||||
keys = []
|
||||
for key in value:
|
||||
keys.append(key)
|
||||
return keys
|
||||
|
||||
|
||||
def _is_array(value):
|
||||
return isinstance(value, list) or isinstance(value, tuple)
|
||||
|
||||
|
||||
def _is_object(value):
|
||||
return isinstance(value, dict)
|
||||
|
||||
|
||||
def _is_string(value):
|
||||
return isinstance(value, str)
|
||||
|
||||
|
||||
def _index(known, input, value):
|
||||
input.append(value)
|
||||
index = str(len(input) - 1)
|
||||
known.key.append(value)
|
||||
known.value.append(index)
|
||||
return index
|
||||
|
||||
|
||||
def _loop(keys, input, known, output):
|
||||
for key in keys:
|
||||
value = output[key]
|
||||
if isinstance(value, _String):
|
||||
_ref(key, input[int(value.value)], input, known, output)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _ref(key, value, input, known, output):
|
||||
if _is_array(value) and not value in known:
|
||||
known.append(value)
|
||||
value = _loop(_array_keys(value), input, known, value)
|
||||
elif _is_object(value) and not value in known:
|
||||
known.append(value)
|
||||
value = _loop(_object_keys(value), input, known, value)
|
||||
|
||||
output[key] = value
|
||||
|
||||
|
||||
def _relate(known, input, value):
|
||||
if _is_string(value) or _is_array(value) or _is_object(value):
|
||||
try:
|
||||
return known.value[known.key.index(value)]
|
||||
except:
|
||||
return _index(known, input, value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _transform(known, input, value):
|
||||
if _is_array(value):
|
||||
output = []
|
||||
for val in value:
|
||||
output.append(_relate(known, input, val))
|
||||
return output
|
||||
|
||||
if _is_object(value):
|
||||
obj = {}
|
||||
for key in value:
|
||||
obj[key] = _relate(known, input, value[key])
|
||||
return obj
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _wrap(value):
|
||||
if _is_string(value):
|
||||
return _String(value)
|
||||
|
||||
if _is_array(value):
|
||||
i = 0
|
||||
for val in value:
|
||||
value[i] = _wrap(val)
|
||||
i += 1
|
||||
|
||||
elif _is_object(value):
|
||||
for key in value:
|
||||
value[key] = _wrap(value[key])
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def parse(value, *args, **kwargs):
|
||||
json = _json.loads(value, *args, **kwargs)
|
||||
wrapped = []
|
||||
for value in json:
|
||||
wrapped.append(_wrap(value))
|
||||
|
||||
input = []
|
||||
for value in wrapped:
|
||||
if isinstance(value, _String):
|
||||
input.append(value.value)
|
||||
else:
|
||||
input.append(value)
|
||||
|
||||
value = input[0]
|
||||
|
||||
if _is_array(value):
|
||||
return _loop(_array_keys(value), input, [value], value)
|
||||
|
||||
if _is_object(value):
|
||||
return _loop(_object_keys(value), input, [value], value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def stringify(value, *args, **kwargs):
|
||||
known = _Known()
|
||||
input = []
|
||||
output = []
|
||||
i = int(_index(known, input, value))
|
||||
while i < len(input):
|
||||
output.append(_transform(known, input, input[i]))
|
||||
i += 1
|
||||
return _json.dumps(output, *args, **kwargs)
|
||||
@@ -1,11 +1,15 @@
|
||||
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:
|
||||
@@ -32,24 +36,21 @@ if RUNNING_IN_WORKER:
|
||||
)
|
||||
|
||||
try:
|
||||
globalThis.SharedArrayBuffer.new(4)
|
||||
import js
|
||||
|
||||
window = polyscript.xworker.window
|
||||
document = window.document
|
||||
js.document = document
|
||||
# this is the same as js_import on main and it lands modules on main
|
||||
js_import = window.Function(
|
||||
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
|
||||
)()
|
||||
except:
|
||||
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",
|
||||
)
|
||||
message = "Unable to use `window` or `document` -> https://docs.pyscript.net/latest/faq/#sharedarraybuffer"
|
||||
globalThis.console.warn(message)
|
||||
window = NotSupported("pyscript.window", message)
|
||||
document = NotSupported("pyscript.document", message)
|
||||
js_import = None
|
||||
|
||||
sync = polyscript.xworker.sync
|
||||
|
||||
@@ -60,7 +61,7 @@ if RUNNING_IN_WORKER:
|
||||
|
||||
else:
|
||||
import _pyscript
|
||||
from _pyscript import PyWorker
|
||||
from _pyscript import PyWorker, js_import
|
||||
|
||||
window = globalThis
|
||||
document = globalThis.document
|
||||
|
||||
60
pyscript.core/src/stdlib/pyscript/storage.py
Normal file
60
pyscript.core/src/stdlib/pyscript/storage.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from polyscript import storage as _storage
|
||||
from pyscript.flatted import parse as _parse
|
||||
from pyscript.flatted import stringify as _stringify
|
||||
|
||||
|
||||
# convert a Python value into an IndexedDB compatible entry
|
||||
def _to_idb(value):
|
||||
if value is None:
|
||||
return _stringify(["null", 0])
|
||||
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
||||
return _stringify(["generic", value])
|
||||
if isinstance(value, bytearray):
|
||||
return _stringify(["bytearray", [v for v in value]])
|
||||
if isinstance(value, memoryview):
|
||||
return _stringify(["memoryview", [v for v in value]])
|
||||
raise TypeError(f"Unexpected value: {value}")
|
||||
|
||||
|
||||
# convert an IndexedDB compatible entry into a Python value
|
||||
def _from_idb(value):
|
||||
(
|
||||
kind,
|
||||
result,
|
||||
) = _parse(value)
|
||||
if kind == "null":
|
||||
return None
|
||||
if kind == "generic":
|
||||
return result
|
||||
if kind == "bytearray":
|
||||
return bytearray(result)
|
||||
if kind == "memoryview":
|
||||
return memoryview(bytearray(result))
|
||||
return value
|
||||
|
||||
|
||||
class Storage(dict):
|
||||
def __init__(self, store):
|
||||
super().__init__({k: _from_idb(v) for k, v in store.entries()})
|
||||
self.__store__ = store
|
||||
|
||||
def __delitem__(self, attr):
|
||||
self.__store__.delete(attr)
|
||||
super().__delitem__(attr)
|
||||
|
||||
def __setitem__(self, attr, value):
|
||||
self.__store__.set(attr, _to_idb(value))
|
||||
super().__setitem__(attr, value)
|
||||
|
||||
def clear(self):
|
||||
self.__store__.clear()
|
||||
super().clear()
|
||||
|
||||
async def sync(self):
|
||||
await self.__store__.sync()
|
||||
|
||||
|
||||
async def storage(name="", storage_class=Storage):
|
||||
if not name:
|
||||
raise ValueError("The storage name must be defined")
|
||||
return storage_class(await _storage(f"@pyscript/{name}"))
|
||||
@@ -1,3 +1,15 @@
|
||||
import js
|
||||
|
||||
|
||||
def as_bytearray(buffer):
|
||||
ui8a = js.Uint8Array.new(buffer)
|
||||
size = ui8a.length
|
||||
ba = bytearray(size)
|
||||
for i in range(0, size):
|
||||
ba[i] = ui8a[i]
|
||||
return ba
|
||||
|
||||
|
||||
class NotSupported:
|
||||
"""
|
||||
Small helper that raises exceptions if you try to get/set any attribute on
|
||||
|
||||
1176
pyscript.core/src/stdlib/pyscript/web.py
Normal file
1176
pyscript.core/src/stdlib/pyscript/web.py
Normal file
File diff suppressed because it is too large
Load Diff
69
pyscript.core/src/stdlib/pyscript/websocket.py
Normal file
69
pyscript.core/src/stdlib/pyscript/websocket.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import js
|
||||
from pyscript.ffi import create_proxy
|
||||
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:
|
||||
# Pyodide fails at setting socket[t] directly
|
||||
setattr(socket, t, create_proxy(kw[t]))
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._ws, attr)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if attr == "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)
|
||||
43
pyscript.core/src/stdlib/pyscript/workers.py
Normal file
43
pyscript.core/src/stdlib/pyscript/workers.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import js as _js
|
||||
from polyscript import workers as _workers
|
||||
|
||||
_get = _js.Reflect.get
|
||||
|
||||
|
||||
def _set(script, name, value=""):
|
||||
script.setAttribute(name, value)
|
||||
|
||||
|
||||
# this solves an inconsistency between Pyodide and MicroPython
|
||||
# @see https://github.com/pyscript/pyscript/issues/2106
|
||||
class _ReadOnlyProxy:
|
||||
def __getitem__(self, name):
|
||||
return _get(_workers, name)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return _get(_workers, name)
|
||||
|
||||
|
||||
workers = _ReadOnlyProxy()
|
||||
|
||||
|
||||
async def create_named_worker(src="", name="", config=None, type="py"):
|
||||
from json import dumps
|
||||
|
||||
if not src:
|
||||
raise ValueError("Named workers require src")
|
||||
|
||||
if not name:
|
||||
raise ValueError("Named workers require a name")
|
||||
|
||||
s = _js.document.createElement("script")
|
||||
s.type = type
|
||||
s.src = src
|
||||
_set(s, "worker")
|
||||
_set(s, "name", name)
|
||||
|
||||
if config:
|
||||
_set(s, "config", isinstance(config, str) and config or dumps(config))
|
||||
|
||||
_js.document.body.append(s)
|
||||
return await workers[name]
|
||||
@@ -1 +0,0 @@
|
||||
from .pydom import dom as pydom
|
||||
@@ -1,95 +0,0 @@
|
||||
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,550 +0,0 @@
|
||||
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 pyscript import display, document, window
|
||||
|
||||
alert = window.alert
|
||||
|
||||
|
||||
class BaseElement:
|
||||
def __init__(self, js_element):
|
||||
self._js = js_element
|
||||
self._parent = None
|
||||
self.style = StyleProxy(self)
|
||||
self._proxies = {}
|
||||
|
||||
def __eq__(self, obj):
|
||||
"""Check if the element is the same as the other element by comparing
|
||||
the underlying JS element"""
|
||||
return isinstance(obj, BaseElement) and obj._js == self._js
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
if self._parent:
|
||||
return self._parent
|
||||
|
||||
if self._js.parentElement:
|
||||
self._parent = self.__class__(self._js.parentElement)
|
||||
|
||||
return self._parent
|
||||
|
||||
@property
|
||||
def __class(self):
|
||||
return self.__class__ if self.__class__ != PyDom else Element
|
||||
|
||||
def create(self, type_, is_child=True, classes=None, html=None, label=None):
|
||||
js_el = document.createElement(type_)
|
||||
element = self.__class(js_el)
|
||||
|
||||
if classes:
|
||||
for class_ in classes:
|
||||
element.add_class(class_)
|
||||
|
||||
if html is not None:
|
||||
element.html = html
|
||||
|
||||
if label is not None:
|
||||
element.label = label
|
||||
|
||||
if is_child:
|
||||
self.append(element)
|
||||
|
||||
return element
|
||||
|
||||
def find(self, selector):
|
||||
"""Return an ElementCollection representing all the child elements that
|
||||
match the specified selector.
|
||||
|
||||
Args:
|
||||
selector (str): A string containing a selector expression
|
||||
|
||||
Returns:
|
||||
ElementCollection: A collection of elements matching the selector
|
||||
"""
|
||||
elements = self._js.querySelectorAll(selector)
|
||||
if not elements:
|
||||
return None
|
||||
return ElementCollection([Element(el) for el in elements])
|
||||
|
||||
|
||||
class Element(BaseElement):
|
||||
@property
|
||||
def children(self):
|
||||
return [self.__class__(el) for el in self._js.children]
|
||||
|
||||
def append(self, child):
|
||||
# TODO: this is Pyodide specific for now!!!!!!
|
||||
# if we get passed a JSProxy Element directly we just map it to the
|
||||
# higher level Python element
|
||||
if isinstance(child, JsProxy):
|
||||
return self.append(Element(child))
|
||||
|
||||
elif isinstance(child, Element):
|
||||
self._js.appendChild(child._js)
|
||||
|
||||
return child
|
||||
|
||||
elif isinstance(child, ElementCollection):
|
||||
for el in child:
|
||||
self.append(el)
|
||||
|
||||
# -------- Pythonic Interface to Element -------- #
|
||||
@property
|
||||
def html(self):
|
||||
return self._js.innerHTML
|
||||
|
||||
@html.setter
|
||||
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
|
||||
# handle this specifica use case. Just not support for now?
|
||||
if self._js.tagName == "TEMPLATE":
|
||||
warnings.warn(
|
||||
"Content attribute not supported for template elements.", stacklevel=2
|
||||
)
|
||||
return None
|
||||
return self._js.innerHTML
|
||||
|
||||
@content.setter
|
||||
def content(self, value):
|
||||
# TODO: (same comment as above)
|
||||
if self._js.tagName == "TEMPLATE":
|
||||
warnings.warn(
|
||||
"Content attribute not supported for template elements.", stacklevel=2
|
||||
)
|
||||
return
|
||||
|
||||
display(value, target=self.id)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._js.id
|
||||
|
||||
@id.setter
|
||||
def id(self, value):
|
||||
self._js.id = value
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
if "options" in self._proxies:
|
||||
return self._proxies["options"]
|
||||
|
||||
if not self._js.tagName.lower() in {"select", "datalist", "optgroup"}:
|
||||
raise AttributeError(
|
||||
f"Element {self._js.tagName} has no options attribute."
|
||||
)
|
||||
self._proxies["options"] = OptionsProxy(self)
|
||||
return self._proxies["options"]
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._js.value
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
# in order to avoid confusion to the user, we don't allow setting the
|
||||
# value of elements that don't have a value attribute
|
||||
if not hasattr(self._js, "value"):
|
||||
raise AttributeError(
|
||||
f"Element {self._js.tagName} has no value attribute. If you want to "
|
||||
"force a value attribute, set it directly using the `_js.value = <value>` "
|
||||
"javascript API attribute instead."
|
||||
)
|
||||
self._js.value = value
|
||||
|
||||
@property
|
||||
def selected(self):
|
||||
return self._js.selected
|
||||
|
||||
@selected.setter
|
||||
def selected(self, value):
|
||||
# in order to avoid confusion to the user, we don't allow setting the
|
||||
# value of elements that don't have a value attribute
|
||||
if not hasattr(self._js, "selected"):
|
||||
raise AttributeError(
|
||||
f"Element {self._js.tagName} has no value attribute. If you want to "
|
||||
"force a value attribute, set it directly using the `_js.value = <value>` "
|
||||
"javascript API attribute instead."
|
||||
)
|
||||
self._js.selected = value
|
||||
|
||||
def clone(self, new_id=None):
|
||||
clone = Element(self._js.cloneNode(True))
|
||||
clone.id = new_id
|
||||
|
||||
return clone
|
||||
|
||||
def remove_class(self, classname):
|
||||
classList = self._js.classList
|
||||
if isinstance(classname, list):
|
||||
classList.remove(*classname)
|
||||
else:
|
||||
classList.remove(classname)
|
||||
return self
|
||||
|
||||
def add_class(self, classname):
|
||||
classList = self._js.classList
|
||||
if isinstance(classname, list):
|
||||
classList.add(*classname)
|
||||
else:
|
||||
self._js.classList.add(classname)
|
||||
return self
|
||||
|
||||
@property
|
||||
def classes(self):
|
||||
classes = self._js.classList.values()
|
||||
return [x for x in classes]
|
||||
|
||||
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
|
||||
allows to access to add and remove options by using the `add` and `remove` methods.
|
||||
"""
|
||||
|
||||
def __init__(self, element: Element) -> None:
|
||||
self._element = element
|
||||
if self._element._js.tagName.lower() != "select":
|
||||
raise AttributeError(
|
||||
f"Element {self._element._js.tagName} has no options attribute."
|
||||
)
|
||||
|
||||
def add(
|
||||
self,
|
||||
value: Any = None,
|
||||
html: str = None,
|
||||
text: str = None,
|
||||
before: Element | int = None,
|
||||
**kws,
|
||||
) -> None:
|
||||
"""Add a new option to the select element"""
|
||||
# create the option element and set the attributes
|
||||
option = document.createElement("option")
|
||||
if value is not None:
|
||||
kws["value"] = value
|
||||
if html is not None:
|
||||
option.innerHTML = html
|
||||
if text is not None:
|
||||
kws["text"] = text
|
||||
|
||||
for key, value in kws.items():
|
||||
option.setAttribute(key, value)
|
||||
|
||||
if before:
|
||||
if isinstance(before, Element):
|
||||
before = before._js
|
||||
|
||||
self._element._js.add(option, before)
|
||||
|
||||
def remove(self, item: int) -> None:
|
||||
"""Remove the option at the specified index"""
|
||||
self._element._js.remove(item)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Remove all the options"""
|
||||
for i in range(len(self)):
|
||||
self.remove(0)
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
"""Return the list of options"""
|
||||
return [Element(opt) for opt in self._element._js.options]
|
||||
|
||||
@property
|
||||
def selected(self):
|
||||
"""Return the selected option"""
|
||||
return self.options[self._element._js.selectedIndex]
|
||||
|
||||
def __iter__(self):
|
||||
yield from self.options
|
||||
|
||||
def __len__(self):
|
||||
return len(self.options)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.options[key]
|
||||
|
||||
|
||||
class StyleProxy: # (dict):
|
||||
def __init__(self, element: Element) -> None:
|
||||
self._element = element
|
||||
|
||||
@cached_property
|
||||
def _style(self):
|
||||
return self._element._js.style
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._style.getPropertyValue(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._style.setProperty(key, value)
|
||||
|
||||
def remove(self, key):
|
||||
self._style.removeProperty(key)
|
||||
|
||||
def set(self, **kws):
|
||||
for k, v in kws.items():
|
||||
self._element._js.style.setProperty(k, v)
|
||||
|
||||
# CSS Properties
|
||||
# Reference: https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L3799C1-L5005C2
|
||||
# Following prperties automatically generated from the above reference using
|
||||
# tools/codegen_css_proxy.py
|
||||
@property
|
||||
def visible(self):
|
||||
return self._element._js.style.visibility
|
||||
|
||||
@visible.setter
|
||||
def visible(self, value):
|
||||
self._element._js.style.visibility = value
|
||||
|
||||
|
||||
class StyleCollection:
|
||||
def __init__(self, collection: "ElementCollection") -> None:
|
||||
self._collection = collection
|
||||
|
||||
def __get__(self, obj, objtype=None):
|
||||
return obj._get_attribute("style")
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._collection._get_attribute("style")[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
for element in self._collection._elements:
|
||||
element.style[key] = value
|
||||
|
||||
def remove(self, key):
|
||||
for element in self._collection._elements:
|
||||
element.style.remove(key)
|
||||
|
||||
|
||||
class ElementCollection:
|
||||
def __init__(self, elements: [Element]) -> None:
|
||||
self._elements = elements
|
||||
self.style = StyleCollection(self)
|
||||
|
||||
def __getitem__(self, key):
|
||||
# If it's an integer we use it to access the elements in the collection
|
||||
if isinstance(key, int):
|
||||
return self._elements[key]
|
||||
# If it's a slice we use it to support slice operations over the elements
|
||||
# in the collection
|
||||
elif isinstance(key, slice):
|
||||
return ElementCollection(self._elements[key])
|
||||
|
||||
# If it's anything else (basically a string) we use it as a selector
|
||||
# TODO: Write tests!
|
||||
elements = self._element.querySelectorAll(key)
|
||||
return ElementCollection([Element(el) for el in elements])
|
||||
|
||||
def __len__(self):
|
||||
return len(self._elements)
|
||||
|
||||
def __eq__(self, obj):
|
||||
"""Check if the element is the same as the other element by comparing
|
||||
the underlying JS element"""
|
||||
return isinstance(obj, ElementCollection) and obj._elements == self._elements
|
||||
|
||||
def _get_attribute(self, attr, index=None):
|
||||
if index is None:
|
||||
return [getattr(el, attr) for el in self._elements]
|
||||
|
||||
# As JQuery, when getting an attr, only return it for the first element
|
||||
return getattr(self._elements[index], attr)
|
||||
|
||||
def _set_attribute(self, attr, value):
|
||||
for el in self._elements:
|
||||
setattr(el, attr, value)
|
||||
|
||||
@property
|
||||
def html(self):
|
||||
return self._get_attribute("html")
|
||||
|
||||
@html.setter
|
||||
def html(self, value):
|
||||
self._set_attribute("html", value)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._get_attribute("value")
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
self._set_attribute("value", value)
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self._elements
|
||||
|
||||
def __iter__(self):
|
||||
yield from self._elements
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}"
|
||||
|
||||
|
||||
class DomScope:
|
||||
def __getattr__(self, __name: str):
|
||||
element = document[f"#{__name}"]
|
||||
if element:
|
||||
return element[0]
|
||||
|
||||
|
||||
class PyDom(BaseElement):
|
||||
# Add objects we want to expose to the DOM namespace since this class instance is being
|
||||
# remapped as "the module" itself
|
||||
BaseElement = BaseElement
|
||||
Element = Element
|
||||
ElementCollection = ElementCollection
|
||||
|
||||
def __init__(self):
|
||||
# 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)
|
||||
|
||||
def create(self, type_, classes=None, html=None):
|
||||
return super().create(type_, is_child=False, classes=classes, html=html)
|
||||
|
||||
def __getitem__(self, key):
|
||||
elements = self._js.querySelectorAll(key)
|
||||
if not elements:
|
||||
return None
|
||||
return ElementCollection([Element(el) for el in elements])
|
||||
|
||||
|
||||
dom = PyDom()
|
||||
72
pyscript.core/src/storage.js
Normal file
72
pyscript.core/src/storage.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ArrayBuffer, TypedArray } from "sabayon/shared";
|
||||
import IDBMapSync from "@webreflection/idb-map/sync";
|
||||
import { parse, stringify } from "flatted";
|
||||
|
||||
const to_idb = (value) => {
|
||||
if (value == null) return stringify(["null", 0]);
|
||||
/* eslint-disable no-fallthrough */
|
||||
switch (typeof value) {
|
||||
case "object": {
|
||||
if (value instanceof TypedArray)
|
||||
return stringify(["memoryview", [...value]]);
|
||||
if (value instanceof ArrayBuffer)
|
||||
return stringify(["bytearray", [...new Uint8Array(value)]]);
|
||||
}
|
||||
case "string":
|
||||
case "number":
|
||||
case "boolean":
|
||||
return stringify(["generic", value]);
|
||||
default:
|
||||
throw new TypeError(`Unexpected value: ${String(value)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const from_idb = (value) => {
|
||||
const [kind, result] = parse(value);
|
||||
if (kind === "null") return null;
|
||||
if (kind === "generic") return result;
|
||||
if (kind === "bytearray") return new Uint8Array(value).buffer;
|
||||
if (kind === "memoryview") return new Uint8Array(value);
|
||||
return value;
|
||||
};
|
||||
|
||||
// this export simulate pyscript.storage exposed in the Python world
|
||||
export const storage = async (name) => {
|
||||
if (!name) throw new SyntaxError("The storage name must be defined");
|
||||
|
||||
const store = new IDBMapSync(`@pyscript/${name}`);
|
||||
const map = new Map();
|
||||
await store.sync();
|
||||
for (const [k, v] of store.entries()) map.set(k, from_idb(v));
|
||||
|
||||
const clear = () => {
|
||||
map.clear();
|
||||
store.clear();
|
||||
};
|
||||
|
||||
const sync = async () => {
|
||||
await store.sync();
|
||||
};
|
||||
|
||||
return new Proxy(map, {
|
||||
ownKeys: (map) => [...map.keys()],
|
||||
has: (map, name) => map.has(name),
|
||||
get: (map, name) => {
|
||||
if (name === "clear") return clear;
|
||||
if (name === "sync") return sync;
|
||||
return map.get(name);
|
||||
},
|
||||
set: (map, name, value) => {
|
||||
map.set(name, value);
|
||||
store.set(name, to_idb(value));
|
||||
return true;
|
||||
},
|
||||
deleteProperty: (map, name) => {
|
||||
if (map.has(name)) {
|
||||
map.delete(name);
|
||||
store.delete(name);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<py-script async>
|
||||
import asyncio
|
||||
print('foo')
|
||||
await asyncio.sleep(1)
|
||||
print('bar')
|
||||
</py-script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,39 +0,0 @@
|
||||
// PyScript Error Plugin
|
||||
import { hooks } from '@pyscript/core';
|
||||
|
||||
hooks.onBeforeRun.add(function override(pyScript) {
|
||||
// be sure this override happens only once
|
||||
hooks.onBeforeRun.delete(override);
|
||||
|
||||
// trap generic `stderr` to propagate to it regardless
|
||||
const { stderr } = pyScript.io;
|
||||
|
||||
// override it with our own logic
|
||||
pyScript.io.stderr = (...args) => {
|
||||
// grab the message of the first argument (Error)
|
||||
const [ { message } ] = args;
|
||||
// show it
|
||||
notify(message);
|
||||
// still let other plugins or PyScript itself do the rest
|
||||
return stderr(...args);
|
||||
};
|
||||
});
|
||||
|
||||
// Error hook utilities
|
||||
|
||||
// Custom function to show notifications
|
||||
function notify(message) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = message;
|
||||
div.style.cssText = `
|
||||
border: 1px solid red;
|
||||
background: #ffdddd;
|
||||
color: black;
|
||||
font-family: courier, monospace;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
`;
|
||||
document.body.append(div);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyDom Example</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" 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,33 +0,0 @@
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime as dt
|
||||
|
||||
from pyscript import display, when
|
||||
from pyweb import pydom
|
||||
|
||||
|
||||
@when("click", "#just-a-button")
|
||||
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):
|
||||
btn = pydom["#result"]
|
||||
btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}"
|
||||
|
||||
|
||||
@when("click", "#color-reset-button")
|
||||
def reset_color(*args, **kwargs):
|
||||
pydom["#result"].style["background-color"] = "white"
|
||||
|
||||
|
||||
# btn_reset = pydom["#color-reset-button"][0].when('click', reset_color)
|
||||
@@ -1,19 +0,0 @@
|
||||
<!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>
|
||||
18
pyscript.core/tests/index.html
Normal file
18
pyscript.core/tests/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript tests</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; }
|
||||
a {
|
||||
display: block;
|
||||
transition: opacity .3s;
|
||||
}
|
||||
a, span { opacity: .7; }
|
||||
a:hover { opacity: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body><ul><li><strong><a href="./config/index.html">config</a></strong><ul><li><a href="./config/ambiguous-config.html">ambiguous-config<small>.html</small></a></li><li><a href="./config/same-config.html">same-config<small>.html</small></a></li><li><a href="./config/too-many-config.html">too-many-config<small>.html</small></a></li><li><a href="./config/too-many-py-config.html">too-many-py-config<small>.html</small></a></li></ul></li><li><strong><a href="./issue-7015/index.html">issue-7015</a></strong></li><li><strong><span>js-integration</span></strong><ul><li><a href="./js-integration/async-listener.html">async-listener<small>.html</small></a></li><li><a href="./js-integration/config-url.html">config-url<small>.html</small></a></li><li><strong><a href="./js-integration/fetch/index.html">fetch</a></strong></li><li><a href="./js-integration/ffi.html">ffi<small>.html</small></a></li><li><a href="./js-integration/hooks.html">hooks<small>.html</small></a></li><li><strong><a href="./js-integration/issue-2093/index.html">issue-2093</a></strong></li><li><a href="./js-integration/js-storage.html">js-storage<small>.html</small></a></li><li><a href="./js-integration/js_modules.html">js_modules<small>.html</small></a></li><li><strong><a href="./js-integration/loader/index.html">loader</a></strong></li><li><a href="./js-integration/mpy.html">mpy<small>.html</small></a></li><li><a href="./js-integration/py-terminal-main.html">py-terminal-main<small>.html</small></a></li><li><a href="./js-integration/py-terminal-worker.html">py-terminal-worker<small>.html</small></a></li><li><a href="./js-integration/py-terminal.html">py-terminal<small>.html</small></a></li><li><a href="./js-integration/py-terminals.html">py-terminals<small>.html</small></a></li><li><a href="./js-integration/storage.html">storage<small>.html</small></a></li><li><strong><a href="./js-integration/workers/index.html">workers</a></strong><ul><li><a href="./js-integration/workers/named.html">named<small>.html</small></a></li></ul></li></ul></li><li><strong><a href="./manual/index.html">manual</a></strong><ul><li><a href="./manual/all-done.html">all-done<small>.html</small></a></li><li><a href="./manual/async.html">async<small>.html</small></a></li><li><a href="./manual/camera.html">camera<small>.html</small></a></li><li><a href="./manual/click.html">click<small>.html</small></a></li><li><a href="./manual/code-a-part.html">code-a-part<small>.html</small></a></li><li><a href="./manual/combo.html">combo<small>.html</small></a></li><li><a href="./manual/config.html">config<small>.html</small></a></li><li><a href="./manual/create-element.html">create-element<small>.html</small></a></li><li><a href="./manual/dialog.html">dialog<small>.html</small></a></li><li><a href="./manual/display.html">display<small>.html</small></a></li><li><a href="./manual/error.html">error<small>.html</small></a></li><li><a href="./manual/html-decode.html">html-decode<small>.html</small></a></li><li><a href="./manual/input.html">input<small>.html</small></a></li><li><a href="./manual/interpreter.html">interpreter<small>.html</small></a></li><li><a href="./manual/multi.html">multi<small>.html</small></a></li><li><a href="./manual/multiple-editors.html">multiple-editors<small>.html</small></a></li><li><a href="./manual/no-error.html">no-error<small>.html</small></a></li><li><a href="./manual/py-editor-failure.html">py-editor-failure<small>.html</small></a></li><li><a href="./manual/py-editor.html">py-editor<small>.html</small></a></li><li><a href="./manual/py_modules.html">py_modules<small>.html</small></a></li><li><a href="./manual/split-config.html">split-config<small>.html</small></a></li><li><a href="./manual/target.html">target<small>.html</small></a></li><li><a href="./manual/test_display_HTML.html">test_display_HTML<small>.html</small></a></li><li><a href="./manual/test_when.html">test_when<small>.html</small></a></li><li><a href="./manual/worker.html">worker<small>.html</small></a></li></ul></li><li><strong><a href="./no_sab/index.html">no_sab</a></strong></li><li><strong><a href="./piratical/index.html">piratical</a></strong></li><li><strong><a href="./py-editor/index.html">py-editor</a></strong><ul><li><a href="./py-editor/issue-2056.html">issue-2056<small>.html</small></a></li><li><a href="./py-editor/service-worker.html">service-worker<small>.html</small></a></li></ul></li><li><strong><a href="./py-terminals/index.html">py-terminals</a></strong><ul><li><a href="./py-terminals/no-repl.html">no-repl<small>.html</small></a></li><li><a href="./py-terminals/repl.html">repl<small>.html</small></a></li></ul></li><li><strong><a href="./pyscript_dom/index.html">pyscript_dom</a></strong></li><li><strong><a href="./service-worker/index.html">service-worker</a></strong></li><li><strong><a href="./ui/index.html">ui</a></strong><ul><li><a href="./ui/gallery.html">gallery<small>.html</small></a></li></ul></li></ul></body>
|
||||
</html>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('MicroPython display', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/test/mpy.html');
|
||||
await page.goto('http://localhost:8080/tests/js-integration/mpy.html');
|
||||
await page.waitForSelector('html.done.worker');
|
||||
const body = await page.evaluate(() => document.body.innerText);
|
||||
await expect(body.trim()).toBe([
|
||||
@@ -18,7 +18,7 @@ test('MicroPython hooks', async ({ page }) => {
|
||||
if (!text.startsWith('['))
|
||||
logs.push(text);
|
||||
});
|
||||
await page.goto('http://localhost:8080/test/hooks.html');
|
||||
await page.goto('http://localhost:8080/tests/js-integration/hooks.html');
|
||||
await page.waitForSelector('html.done.worker');
|
||||
await expect(logs.join('\n')).toBe([
|
||||
'main onReady',
|
||||
@@ -43,7 +43,7 @@ test('MicroPython + Pyodide js_modules', async ({ page }) => {
|
||||
if (!text.startsWith('['))
|
||||
logs.push(text);
|
||||
});
|
||||
await page.goto('http://localhost:8080/test/js_modules.html');
|
||||
await page.goto('http://localhost:8080/tests/js-integration/js_modules.html');
|
||||
await page.waitForSelector('html.done');
|
||||
await expect(logs.length).toBe(6);
|
||||
await expect(logs[0]).toBe(logs[1]);
|
||||
@@ -53,38 +53,64 @@ test('MicroPython + Pyodide js_modules', async ({ page }) => {
|
||||
});
|
||||
|
||||
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.goto('http://localhost:8080/tests/js-integration/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.goto('http://localhost:8080/tests/js-integration/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.goto('http://localhost:8080/tests/js-integration/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.goto('http://localhost:8080/tests/js-integration/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.goto('http://localhost:8080/tests/js-integration/fetch/index.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
});
|
||||
|
||||
test('MicroPython + Pyodide ffi', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/test/ffi.html');
|
||||
await page.goto('http://localhost:8080/tests/js-integration/ffi.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
});
|
||||
|
||||
test('MicroPython + Storage', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/storage.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
test('MicroPython + JS Storage', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/js-storage.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
test('MicroPython + workers', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/workers/index.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
});
|
||||
|
||||
test('MicroPython Editor setup error', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/issue-2093/index.html');
|
||||
await page.waitForSelector('html.errored');
|
||||
});
|
||||
|
||||
test('MicroPython async @when listener', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/async-listener.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
test('Pyodide loader', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/js-integration/loader/index.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
const body = await page.evaluate(() => document.body.textContent);
|
||||
await expect(body.includes('Loaded Pyodide')).toBe(true);
|
||||
});
|
||||
@@ -17,7 +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")
|
||||
TEST = ROOT.join("pyscript.core").join("tests")
|
||||
|
||||
|
||||
def params_with_marks(params):
|
||||
@@ -212,7 +212,7 @@ class PyScriptTest:
|
||||
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)
|
||||
tmpdir.join("tests").mksymlinkto(TEST)
|
||||
|
||||
# create a symlink to the favicon, so that we can use it in the HTML
|
||||
self.tmpdir.chdir()
|
||||
|
||||
@@ -186,12 +186,12 @@ class TestSupport(PyScriptTest):
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
"""
|
||||
f"""
|
||||
JS errors found: 2
|
||||
Error: error 1
|
||||
at https://fake_server/mytest.html:.*
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
Error: error 2
|
||||
at https://fake_server/mytest.html:.*
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
@@ -217,12 +217,12 @@ class TestSupport(PyScriptTest):
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
"""
|
||||
f"""
|
||||
JS errors found: 2
|
||||
Error: NOT expected 2
|
||||
at https://fake_server/mytest.html:.*
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
Error: NOT expected 4
|
||||
at https://fake_server/mytest.html:.*
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
@@ -243,15 +243,15 @@ class TestSupport(PyScriptTest):
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
"""
|
||||
f"""
|
||||
The following JS errors were expected but could not be found:
|
||||
- this is not going to be found
|
||||
---
|
||||
The following JS errors were raised but not expected:
|
||||
Error: error 1
|
||||
at https://fake_server/mytest.html:.*
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
Error: error 2
|
||||
at https://fake_server/mytest.html:.*
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
@@ -471,6 +471,8 @@ class TestSupport(PyScriptTest):
|
||||
Test that we capture a 404 in loading a page that does not exist.
|
||||
"""
|
||||
self.goto("this_url_does_not_exist.html")
|
||||
assert [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
] == self.console.all.lines
|
||||
if self.dev_server:
|
||||
error = "Failed to load resource: the server responded with a status of 404 (File not found)"
|
||||
else:
|
||||
error = "Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
assert [error] == self.console.all.lines
|
||||
|
||||
@@ -97,7 +97,7 @@ class TestBasic(PyScriptTest):
|
||||
def test_input_exception(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
<script type="py" async="false">
|
||||
input("what's your name?")
|
||||
</script>
|
||||
"""
|
||||
|
||||
@@ -43,12 +43,12 @@ class TestDisplay(PyScriptTest):
|
||||
def test_consecutive_display(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
<script type="py" async="false">
|
||||
from pyscript import display
|
||||
display('hello 1')
|
||||
</script>
|
||||
<p>hello 2</p>
|
||||
<script type="py">
|
||||
<script type="py" async="false">
|
||||
from pyscript import display
|
||||
display('hello 3')
|
||||
</script>
|
||||
@@ -177,16 +177,16 @@ class TestDisplay(PyScriptTest):
|
||||
def test_consecutive_display_target(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" id="first">
|
||||
<script type="py" id="first" async="false">
|
||||
from pyscript import display
|
||||
display('hello 1')
|
||||
</script>
|
||||
<p>hello in between 1 and 2</p>
|
||||
<script type="py" id="second">
|
||||
<script type="py" id="second" async="false">
|
||||
from pyscript import display
|
||||
display('hello 2', target="second")
|
||||
</script>
|
||||
<script type="py" id="third">
|
||||
<script type="py" id="third" async="false">
|
||||
from pyscript import display
|
||||
display('hello 3')
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,6 @@ from .support import PyScriptTest, with_execution_thread
|
||||
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
|
||||
@@ -14,7 +13,7 @@ class TestSmokeTests(PyScriptTest):
|
||||
|
||||
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")
|
||||
self.goto("tests/pyscript_dom/index.html?-v&-s")
|
||||
assert self.page.title() == "PyDom Test Suite"
|
||||
|
||||
# wait for the test suite to finish
|
||||
|
||||
804
pyscript.core/tests/integration/test_pyweb.py
Normal file
804
pyscript.core/tests/integration/test_pyweb.py
Normal file
@@ -0,0 +1,804 @@
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, only_main, skip_worker
|
||||
|
||||
DEFAULT_ELEMENT_ATTRIBUTES = {
|
||||
"accesskey": "s",
|
||||
"autocapitalize": "off",
|
||||
"autofocus": True,
|
||||
"contenteditable": True,
|
||||
"draggable": True,
|
||||
"enterkeyhint": "go",
|
||||
"hidden": False,
|
||||
"id": "whateverid",
|
||||
"lang": "br",
|
||||
"nonce": "123",
|
||||
"part": "part1:exposed1",
|
||||
"popover": True,
|
||||
"slot": "slot1",
|
||||
"spellcheck": False,
|
||||
"tabindex": 3,
|
||||
"title": "whatevertitle",
|
||||
"translate": "no",
|
||||
"virtualkeyboardpolicy": "manual",
|
||||
}
|
||||
|
||||
INTERPRETERS = ["py", "mpy"]
|
||||
|
||||
|
||||
@pytest.fixture(params=INTERPRETERS)
|
||||
def interpreter(request):
|
||||
return request.param
|
||||
|
||||
|
||||
class TestElements(PyScriptTest):
|
||||
"""Test all elements in the pyweb.ui.elements module.
|
||||
|
||||
This class tests all elements in the pyweb.ui.elements module. It creates
|
||||
an element of each type, both executing in the main thread and in a worker.
|
||||
It runs each test for each interpreter defined in `INTERPRETERS`
|
||||
|
||||
Each individual element test looks for the element properties, sets a value
|
||||
on each the supported properties and checks if the element was created correctly
|
||||
and all it's properties were set correctly.
|
||||
"""
|
||||
|
||||
@property
|
||||
def expected_missing_file_errors(self):
|
||||
# In fake server conditions this test will not throw an error due to missing files.
|
||||
# If we want to skip the test, use:
|
||||
# pytest.skip("Skipping: fake server doesn't throw 404 errors on missing local files.")
|
||||
return (
|
||||
[
|
||||
"Failed to load resource: the server responded with a status of 404 (File not found)"
|
||||
]
|
||||
if self.dev_server
|
||||
else []
|
||||
)
|
||||
|
||||
def _create_el_and_basic_asserts(
|
||||
self,
|
||||
el_type,
|
||||
el_text=None,
|
||||
interpreter="py",
|
||||
properties=None,
|
||||
expected_errors=None,
|
||||
additional_selector_rules=None,
|
||||
):
|
||||
"""Create an element with all its properties set, by running <script type=<interpreter> ... >
|
||||
, and check if the element was created correctly and all its properties were set correctly.
|
||||
"""
|
||||
expected_errors = expected_errors or []
|
||||
if not properties:
|
||||
properties = {}
|
||||
|
||||
def parse_value(v):
|
||||
if isinstance(v, bool):
|
||||
return str(v)
|
||||
|
||||
return f"'{v}'"
|
||||
|
||||
attributes = ""
|
||||
if el_text:
|
||||
attributes += f'"{el_text}",'
|
||||
|
||||
if properties:
|
||||
attributes += ", ".join(
|
||||
[f"{k}={parse_value(v)}" for k, v in properties.items()]
|
||||
)
|
||||
|
||||
# Let's make sure the body of the page is clean first
|
||||
body = self.page.locator("body")
|
||||
assert body.inner_html() == ""
|
||||
|
||||
# Let's make sure the element is not in the page
|
||||
element = self.page.locator(el_type)
|
||||
assert not element.count()
|
||||
|
||||
# Let's create the element
|
||||
code_ = f"""
|
||||
from pyscript import when
|
||||
<script type="{interpreter}">
|
||||
from pyscript.web import page, {el_type}
|
||||
el = {el_type}({attributes})
|
||||
page.body.append(el)
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(code_)
|
||||
|
||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||
# check the return tag from the selector
|
||||
locator_type = el_tag = el_type[:-1] if el_type.endswith("_") else el_type
|
||||
if additional_selector_rules:
|
||||
locator_type += f"{additional_selector_rules}"
|
||||
|
||||
el = self.page.locator(locator_type)
|
||||
tag = el.evaluate("node => node.tagName")
|
||||
assert tag == el_tag.upper()
|
||||
if el_text:
|
||||
assert el.inner_html() == el_text
|
||||
assert el.text_content() == el_text
|
||||
|
||||
# if we expect specific errors, check that they are in the console
|
||||
if expected_errors:
|
||||
for error in expected_errors:
|
||||
assert error in self.console.error.lines
|
||||
else:
|
||||
# if we don't expect errors, check that there are no errors
|
||||
assert self.console.error.lines == []
|
||||
|
||||
if properties:
|
||||
for k, v in properties.items():
|
||||
actual_val = el.evaluate(f"node => node.{k}")
|
||||
assert actual_val == v
|
||||
return el
|
||||
|
||||
def test_a(self, interpreter):
|
||||
a = self._create_el_and_basic_asserts("a", "click me", interpreter)
|
||||
assert a.text_content() == "click me"
|
||||
|
||||
def test_abbr(self, interpreter):
|
||||
abbr = self._create_el_and_basic_asserts(
|
||||
"abbr", "some text", interpreter=interpreter
|
||||
)
|
||||
assert abbr.text_content() == "some text"
|
||||
|
||||
def test_address(self, interpreter):
|
||||
address = self._create_el_and_basic_asserts("address", "some text", interpreter)
|
||||
assert address.text_content() == "some text"
|
||||
|
||||
def test_area(self, interpreter):
|
||||
properties = {
|
||||
"shape": "poly",
|
||||
"coords": "129,0,260,95,129,138",
|
||||
"href": "https://developer.mozilla.org/docs/Web/HTTP",
|
||||
"target": "_blank",
|
||||
"alt": "HTTP",
|
||||
}
|
||||
# TODO: Check why click times out
|
||||
self._create_el_and_basic_asserts(
|
||||
"area", interpreter=interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_article(self, interpreter):
|
||||
self._create_el_and_basic_asserts("article", "some text", interpreter)
|
||||
|
||||
def test_aside(self, interpreter):
|
||||
self._create_el_and_basic_asserts("aside", "some text", interpreter)
|
||||
|
||||
def test_audio(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"audio",
|
||||
interpreter=interpreter,
|
||||
properties={"src": "http://localhost:8080/somefile.ogg", "controls": True},
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
)
|
||||
|
||||
def test_b(self, interpreter):
|
||||
self._create_el_and_basic_asserts("b", "some text", interpreter)
|
||||
|
||||
def test_blockquote(self, interpreter):
|
||||
self._create_el_and_basic_asserts("blockquote", "some text", interpreter)
|
||||
|
||||
def test_br(self, interpreter):
|
||||
self._create_el_and_basic_asserts("br", interpreter=interpreter)
|
||||
|
||||
def test_element_button(self, interpreter):
|
||||
button = self._create_el_and_basic_asserts("button", "click me", interpreter)
|
||||
assert button.inner_html() == "click me"
|
||||
|
||||
def test_element_button_attributes(self, interpreter):
|
||||
button = self._create_el_and_basic_asserts(
|
||||
"button", "click me", interpreter, None
|
||||
)
|
||||
assert button.inner_html() == "click me"
|
||||
|
||||
def test_canvas(self, interpreter):
|
||||
properties = {
|
||||
"height": 100,
|
||||
"width": 120,
|
||||
}
|
||||
# TODO: Check why click times out
|
||||
self._create_el_and_basic_asserts(
|
||||
"canvas", "alt text for canvas", interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_caption(self, interpreter):
|
||||
self._create_el_and_basic_asserts("caption", "some text", interpreter)
|
||||
|
||||
def test_cite(self, interpreter):
|
||||
self._create_el_and_basic_asserts("cite", "some text", interpreter)
|
||||
|
||||
def test_code(self, interpreter):
|
||||
self._create_el_and_basic_asserts("code", "import pyweb", interpreter)
|
||||
|
||||
def test_data(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"data", "some text", interpreter, properties={"value": "123"}
|
||||
)
|
||||
|
||||
def test_datalist(self, interpreter):
|
||||
self._create_el_and_basic_asserts("datalist", "some items", interpreter)
|
||||
|
||||
def test_dd(self, interpreter):
|
||||
self._create_el_and_basic_asserts("dd", "some text", interpreter)
|
||||
|
||||
def test_del_(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"del_", "some text", interpreter, properties={"cite": "http://example.com/"}
|
||||
)
|
||||
|
||||
def test_details(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"details", "some text", interpreter, properties={"open": True}
|
||||
)
|
||||
|
||||
def test_dialog(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"dialog", "some text", interpreter, properties={"open": True}
|
||||
)
|
||||
|
||||
def test_div(self, interpreter):
|
||||
div = self._create_el_and_basic_asserts("div", "click me", interpreter)
|
||||
assert div.inner_html() == "click me"
|
||||
|
||||
def test_dl(self, interpreter):
|
||||
self._create_el_and_basic_asserts("dl", "some text", interpreter)
|
||||
|
||||
def test_dt(self, interpreter):
|
||||
self._create_el_and_basic_asserts("dt", "some text", interpreter)
|
||||
|
||||
def test_em(self, interpreter):
|
||||
self._create_el_and_basic_asserts("em", "some text", interpreter)
|
||||
|
||||
def test_embed(self, interpreter):
|
||||
# NOTE: Types actually matter and embed expects a string for height and width
|
||||
# while other elements expect an int
|
||||
|
||||
# TODO: It's important that we add typing soon to help with the user experience
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.ogg",
|
||||
"type": "video/ogg",
|
||||
"width": "250",
|
||||
"height": "200",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"embed",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
)
|
||||
|
||||
def test_fieldset(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"fieldset", "some text", interpreter, properties={"name": "some name"}
|
||||
)
|
||||
|
||||
def test_figcaption(self, interpreter):
|
||||
self._create_el_and_basic_asserts("figcaption", "some text", interpreter)
|
||||
|
||||
def test_figure(self, interpreter):
|
||||
self._create_el_and_basic_asserts("figure", "some text", interpreter)
|
||||
|
||||
def test_footer(self, interpreter):
|
||||
self._create_el_and_basic_asserts("footer", "some text", interpreter)
|
||||
|
||||
def test_form(self, interpreter):
|
||||
properties = {
|
||||
"action": "https://example.com/submit",
|
||||
"method": "post",
|
||||
"name": "some name",
|
||||
"autocomplete": "on",
|
||||
"rel": "external",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"form", "some text", interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_h1(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h1", "some text", interpreter)
|
||||
|
||||
def test_h2(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h2", "some text", interpreter)
|
||||
|
||||
def test_h3(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h3", "some text", interpreter)
|
||||
|
||||
def test_h4(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h4", "some text", interpreter)
|
||||
|
||||
def test_h5(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h5", "some text", interpreter)
|
||||
|
||||
def test_h6(self, interpreter):
|
||||
self._create_el_and_basic_asserts("h6", "some text", interpreter)
|
||||
|
||||
def test_header(self, interpreter):
|
||||
self._create_el_and_basic_asserts("header", "some text", interpreter)
|
||||
|
||||
def test_hgroup(self, interpreter):
|
||||
self._create_el_and_basic_asserts("hgroup", "some text", interpreter)
|
||||
|
||||
def test_hr(self, interpreter):
|
||||
self._create_el_and_basic_asserts("hr", interpreter=interpreter)
|
||||
|
||||
def test_i(self, interpreter):
|
||||
self._create_el_and_basic_asserts("i", "some text", interpreter)
|
||||
|
||||
def test_iframe(self, interpreter):
|
||||
# TODO: same comment about defining the right types
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.html",
|
||||
"width": "250",
|
||||
"height": "200",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"iframe",
|
||||
interpreter,
|
||||
properties=properties,
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
)
|
||||
|
||||
def test_img(self, interpreter):
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.png",
|
||||
"alt": "some image",
|
||||
"width": 250,
|
||||
"height": 200,
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"img",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
)
|
||||
|
||||
def test_input(self, interpreter):
|
||||
# TODO: we need multiple input tests
|
||||
properties = {
|
||||
"type": "text",
|
||||
"value": "some value",
|
||||
"name": "some name",
|
||||
"autofocus": True,
|
||||
"pattern": "[A-Za-z]{3}",
|
||||
"placeholder": "some placeholder",
|
||||
"required": True,
|
||||
"size": 20,
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"input_", interpreter=interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_ins(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"ins", "some text", interpreter, properties={"cite": "http://example.com/"}
|
||||
)
|
||||
|
||||
def test_kbd(self, interpreter):
|
||||
self._create_el_and_basic_asserts("kbd", "some text", interpreter)
|
||||
|
||||
def test_label(self, interpreter):
|
||||
self._create_el_and_basic_asserts("label", "some text", interpreter)
|
||||
|
||||
def test_legend(self, interpreter):
|
||||
self._create_el_and_basic_asserts("legend", "some text", interpreter)
|
||||
|
||||
def test_li(self, interpreter):
|
||||
self._create_el_and_basic_asserts("li", "some text", interpreter)
|
||||
|
||||
def test_link(self, interpreter):
|
||||
properties = {
|
||||
"href": "http://localhost:8080/somefile.css",
|
||||
"rel": "stylesheet",
|
||||
"type": "text/css",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"link",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
additional_selector_rules="[href='http://localhost:8080/somefile.css']",
|
||||
)
|
||||
|
||||
def test_main(self, interpreter):
|
||||
self._create_el_and_basic_asserts("main", "some text", interpreter)
|
||||
|
||||
def test_map(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"map_", "some text", interpreter, properties={"name": "somemap"}
|
||||
)
|
||||
|
||||
def test_mark(self, interpreter):
|
||||
self._create_el_and_basic_asserts("mark", "some text", interpreter)
|
||||
|
||||
def test_menu(self, interpreter):
|
||||
self._create_el_and_basic_asserts("menu", "some text", interpreter)
|
||||
|
||||
def test_meter(self, interpreter):
|
||||
properties = {
|
||||
"value": 50,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"low": 30,
|
||||
"high": 80,
|
||||
"optimum": 50,
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"meter", "some text", interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_nav(self, interpreter):
|
||||
self._create_el_and_basic_asserts("nav", "some text", interpreter)
|
||||
|
||||
def test_object(self, interpreter):
|
||||
properties = {
|
||||
"data": "http://localhost:8080/somefile.swf",
|
||||
"type": "application/x-shockwave-flash",
|
||||
"width": "250",
|
||||
"height": "200",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"object_",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
def test_ol(self, interpreter):
|
||||
self._create_el_and_basic_asserts("ol", "some text", interpreter)
|
||||
|
||||
def test_optgroup(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"optgroup", "some text", interpreter, properties={"label": "some label"}
|
||||
)
|
||||
|
||||
def test_option(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"option", "some text", interpreter, properties={"value": "some value"}
|
||||
)
|
||||
|
||||
def test_output(self, interpreter):
|
||||
self._create_el_and_basic_asserts("output", "some text", interpreter)
|
||||
|
||||
def test_p(self, interpreter):
|
||||
self._create_el_and_basic_asserts("p", "some text", interpreter)
|
||||
|
||||
def test_picture(self, interpreter):
|
||||
self._create_el_and_basic_asserts("picture", "some text", interpreter)
|
||||
|
||||
def test_pre(self, interpreter):
|
||||
self._create_el_and_basic_asserts("pre", "some text", interpreter)
|
||||
|
||||
def test_progress(self, interpreter):
|
||||
properties = {
|
||||
"value": 50,
|
||||
"max": 100,
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"progress", "some text", interpreter, properties=properties
|
||||
)
|
||||
|
||||
def test_q(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"q", "some text", interpreter, properties={"cite": "http://example.com/"}
|
||||
)
|
||||
|
||||
def test_s(self, interpreter):
|
||||
self._create_el_and_basic_asserts("s", "some text", interpreter)
|
||||
|
||||
# def test_script(self):
|
||||
# self._create_el_and_basic_asserts("script", "some text")
|
||||
|
||||
def test_section(self, interpreter):
|
||||
self._create_el_and_basic_asserts("section", "some text", interpreter)
|
||||
|
||||
def test_select(self, interpreter):
|
||||
self._create_el_and_basic_asserts("select", "some text", interpreter)
|
||||
|
||||
def test_small(self, interpreter):
|
||||
self._create_el_and_basic_asserts("small", "some text", interpreter)
|
||||
|
||||
def test_source(self, interpreter):
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.ogg",
|
||||
"type": "audio/ogg",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"source",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
def test_span(self, interpreter):
|
||||
self._create_el_and_basic_asserts("span", "some text", interpreter)
|
||||
|
||||
def test_strong(self, interpreter):
|
||||
self._create_el_and_basic_asserts("strong", "some text", interpreter)
|
||||
|
||||
def test_style(self, interpreter):
|
||||
self._create_el_and_basic_asserts(
|
||||
"style",
|
||||
"body {background-color: red;}",
|
||||
interpreter,
|
||||
)
|
||||
|
||||
def test_sub(self, interpreter):
|
||||
self._create_el_and_basic_asserts("sub", "some text", interpreter)
|
||||
|
||||
def test_summary(self, interpreter):
|
||||
self._create_el_and_basic_asserts("summary", "some text", interpreter)
|
||||
|
||||
def test_sup(self, interpreter):
|
||||
self._create_el_and_basic_asserts("sup", "some text", interpreter)
|
||||
|
||||
def test_table(self, interpreter):
|
||||
self._create_el_and_basic_asserts("table", "some text", interpreter)
|
||||
|
||||
def test_tbody(self, interpreter):
|
||||
self._create_el_and_basic_asserts("tbody", "some text", interpreter)
|
||||
|
||||
def test_td(self, interpreter):
|
||||
self._create_el_and_basic_asserts("td", "some text", interpreter)
|
||||
|
||||
def test_template(self, interpreter):
|
||||
# We are not checking the content of template since it's sort of
|
||||
# special element
|
||||
self._create_el_and_basic_asserts("template", interpreter=interpreter)
|
||||
|
||||
def test_textarea(self, interpreter):
|
||||
self._create_el_and_basic_asserts("textarea", "some text", interpreter)
|
||||
|
||||
def test_tfoot(self, interpreter):
|
||||
self._create_el_and_basic_asserts("tfoot", "some text", interpreter)
|
||||
|
||||
def test_th(self, interpreter):
|
||||
self._create_el_and_basic_asserts("th", "some text", interpreter)
|
||||
|
||||
def test_thead(self, interpreter):
|
||||
self._create_el_and_basic_asserts("thead", "some text", interpreter)
|
||||
|
||||
def test_time(self, interpreter):
|
||||
self._create_el_and_basic_asserts("time", "some text", interpreter)
|
||||
|
||||
def test_title(self, interpreter):
|
||||
self._create_el_and_basic_asserts("title", "some text", interpreter)
|
||||
|
||||
def test_tr(self, interpreter):
|
||||
self._create_el_and_basic_asserts("tr", "some text", interpreter)
|
||||
|
||||
def test_track(self, interpreter):
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.vtt",
|
||||
"kind": "subtitles",
|
||||
"srclang": "en",
|
||||
"label": "English",
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"track",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
def test_u(self, interpreter):
|
||||
self._create_el_and_basic_asserts("u", "some text", interpreter)
|
||||
|
||||
def test_ul(self, interpreter):
|
||||
self._create_el_and_basic_asserts("ul", "some text", interpreter)
|
||||
|
||||
def test_var(self, interpreter):
|
||||
self._create_el_and_basic_asserts("var", "some text", interpreter)
|
||||
|
||||
def test_video(self, interpreter):
|
||||
properties = {
|
||||
"src": "http://localhost:8080/somefile.ogg",
|
||||
"controls": True,
|
||||
"width": 250,
|
||||
"height": 200,
|
||||
}
|
||||
self._create_el_and_basic_asserts(
|
||||
"video",
|
||||
interpreter=interpreter,
|
||||
properties=properties,
|
||||
expected_errors=self.expected_missing_file_errors,
|
||||
)
|
||||
|
||||
def test_append_py_element(self, interpreter):
|
||||
# Let's make sure the body of the page is clean first
|
||||
body = self.page.locator("body")
|
||||
assert body.inner_html() == ""
|
||||
|
||||
# Let's make sure the element is not in the page
|
||||
element = self.page.locator("div")
|
||||
assert not element.count()
|
||||
|
||||
div_text_content = "Luke, I am your father"
|
||||
p_text_content = "noooooooooo!"
|
||||
# Let's create the element
|
||||
code_ = f"""
|
||||
from pyscript import when
|
||||
<script type="{interpreter}">
|
||||
from pyscript.web import page, div, p
|
||||
|
||||
el = div("{div_text_content}")
|
||||
child = p('{p_text_content}')
|
||||
el.append(child)
|
||||
page.body.append(el)
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(code_)
|
||||
|
||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||
# check the return tag from the selector
|
||||
el = self.page.locator("div")
|
||||
tag = el.evaluate("node => node.tagName")
|
||||
assert tag == "DIV"
|
||||
assert el.text_content() == f"{div_text_content}{p_text_content}"
|
||||
|
||||
assert (
|
||||
el.evaluate("node => node.children.length") == 1
|
||||
), "There should be only 1 child"
|
||||
assert el.evaluate("node => node.children[0].tagName") == "P"
|
||||
assert (
|
||||
el.evaluate("node => node.children[0].parentNode.textContent")
|
||||
== f"{div_text_content}{p_text_content}"
|
||||
)
|
||||
assert el.evaluate("node => node.children[0].textContent") == p_text_content
|
||||
|
||||
def test_append_proxy_element(self, interpreter):
|
||||
# Let's make sure the body of the page is clean first
|
||||
body = self.page.locator("body")
|
||||
assert body.inner_html() == ""
|
||||
|
||||
# Let's make sure the element is not in the page
|
||||
element = self.page.locator("div")
|
||||
assert not element.count()
|
||||
|
||||
div_text_content = "Luke, I am your father"
|
||||
p_text_content = "noooooooooo!"
|
||||
# Let's create the element
|
||||
code_ = f"""
|
||||
from pyscript import when
|
||||
<script type="{interpreter}">
|
||||
from pyscript import document
|
||||
from pyscript.web import page, div, p
|
||||
|
||||
el = div("{div_text_content}")
|
||||
child = document.createElement('P')
|
||||
child.textContent = '{p_text_content}'
|
||||
el.append(child)
|
||||
page.body.append(el)
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(code_)
|
||||
|
||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||
# check the return tag from the selector
|
||||
el = self.page.locator("div")
|
||||
tag = el.evaluate("node => node.tagName")
|
||||
assert tag == "DIV"
|
||||
assert el.text_content() == f"{div_text_content}{p_text_content}"
|
||||
|
||||
assert (
|
||||
el.evaluate("node => node.children.length") == 1
|
||||
), "There should be only 1 child"
|
||||
assert el.evaluate("node => node.children[0].tagName") == "P"
|
||||
assert (
|
||||
el.evaluate("node => node.children[0].parentNode.textContent")
|
||||
== f"{div_text_content}{p_text_content}"
|
||||
)
|
||||
assert el.evaluate("node => node.children[0].textContent") == p_text_content
|
||||
|
||||
def test_append_py_elementcollection(self, interpreter):
|
||||
# Let's make sure the body of the page is clean first
|
||||
body = self.page.locator("body")
|
||||
assert body.inner_html() == ""
|
||||
|
||||
# Let's make sure the element is not in the page
|
||||
element = self.page.locator("div")
|
||||
assert not element.count()
|
||||
|
||||
div_text_content = "Luke, I am your father"
|
||||
p_text_content = "noooooooooo!"
|
||||
p2_text_content = "not me!"
|
||||
# Let's create the element
|
||||
code_ = f"""
|
||||
from pyscript import when
|
||||
<script type="{interpreter}">
|
||||
from pyscript.web import page, div, p, ElementCollection
|
||||
|
||||
el = div("{div_text_content}")
|
||||
child1 = p('{p_text_content}')
|
||||
child2 = p('{p2_text_content}', id='child2')
|
||||
collection = ElementCollection([child1, child2])
|
||||
el.append(collection)
|
||||
page.body.append(el)
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(code_)
|
||||
|
||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||
# check the return tag from the selector
|
||||
el = self.page.locator("div")
|
||||
tag = el.evaluate("node => node.tagName")
|
||||
assert tag == "DIV"
|
||||
parent_full_content = f"{div_text_content}{p_text_content}{p2_text_content}"
|
||||
assert el.text_content() == parent_full_content
|
||||
|
||||
assert (
|
||||
el.evaluate("node => node.children.length") == 2
|
||||
), "There should be only 1 child"
|
||||
assert el.evaluate("node => node.children[0].tagName") == "P"
|
||||
assert (
|
||||
el.evaluate("node => node.children[0].parentNode.textContent")
|
||||
== parent_full_content
|
||||
)
|
||||
assert el.evaluate("node => node.children[0].textContent") == p_text_content
|
||||
|
||||
assert el.evaluate("node => node.children[1].tagName") == "P"
|
||||
assert el.evaluate("node => node.children[1].id") == "child2"
|
||||
assert (
|
||||
el.evaluate("node => node.children[1].parentNode.textContent")
|
||||
== parent_full_content
|
||||
)
|
||||
assert el.evaluate("node => node.children[1].textContent") == p2_text_content
|
||||
|
||||
def test_append_js_element_nodelist(self, interpreter):
|
||||
# Let's make sure the body of the page is clean first
|
||||
body = self.page.locator("body")
|
||||
assert body.inner_html() == ""
|
||||
|
||||
# Let's make sure the element is not in the page
|
||||
element = self.page.locator("div")
|
||||
assert not element.count()
|
||||
|
||||
div_text_content = "Luke, I am your father"
|
||||
p_text_content = "noooooooooo!"
|
||||
p2_text_content = "not me!"
|
||||
# Let's create the element
|
||||
code_ = f"""
|
||||
from pyscript import when
|
||||
<script type="{interpreter}">
|
||||
from pyscript import document
|
||||
from pyscript.web import page, div, p, ElementCollection
|
||||
|
||||
el = div("{div_text_content}")
|
||||
child1 = p('{p_text_content}')
|
||||
child2 = p('{p2_text_content}', id='child2')
|
||||
|
||||
page.body.append(child1)
|
||||
page.body.append(child2)
|
||||
|
||||
nodes = document.querySelectorAll('p')
|
||||
el.append(nodes)
|
||||
|
||||
page.body.append(el)
|
||||
</script>
|
||||
"""
|
||||
self.pyscript_run(code_)
|
||||
|
||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||
# check the return tag from the selector
|
||||
el = self.page.locator("div")
|
||||
tag = el.evaluate("node => node.tagName")
|
||||
assert tag == "DIV"
|
||||
parent_full_content = f"{div_text_content}{p_text_content}{p2_text_content}"
|
||||
assert el.text_content() == parent_full_content
|
||||
|
||||
assert (
|
||||
el.evaluate("node => node.children.length") == 2
|
||||
), "There should be only 1 child"
|
||||
assert el.evaluate("node => node.children[0].tagName") == "P"
|
||||
assert (
|
||||
el.evaluate("node => node.children[0].parentNode.textContent")
|
||||
== parent_full_content
|
||||
)
|
||||
assert el.evaluate("node => node.children[0].textContent") == p_text_content
|
||||
|
||||
assert el.evaluate("node => node.children[1].tagName") == "P"
|
||||
assert el.evaluate("node => node.children[1].id") == "child2"
|
||||
assert (
|
||||
el.evaluate("node => node.children[1].parentNode.textContent")
|
||||
== parent_full_content
|
||||
)
|
||||
assert el.evaluate("node => node.children[1].textContent") == p2_text_content
|
||||
4
pyscript.core/tests/issue-7015/config.toml
Normal file
4
pyscript.core/tests/issue-7015/config.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages = [
|
||||
"https://cdn.holoviz.org/panel/wheels/bokeh-3.5.0-py3-none-any.whl",
|
||||
"https://cdn.holoviz.org/panel/1.5.0-b.2/dist/wheels/panel-1.5.0b2-py3-none-any.whl"
|
||||
]
|
||||
17
pyscript.core/tests/issue-7015/index.html
Normal file
17
pyscript.core/tests/issue-7015/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-3.5.0.js"></script>
|
||||
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.0.min.js"></script>
|
||||
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@holoviz/panel@1.5.0-b.2/dist/panel.min.js"></script>
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" src="main.py" config="config.toml" worker></script>
|
||||
<div id="simple_app"></div>
|
||||
</body>
|
||||
</html>
|
||||
12
pyscript.core/tests/issue-7015/main.py
Normal file
12
pyscript.core/tests/issue-7015/main.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import panel as pn
|
||||
|
||||
pn.extension(sizing_mode="stretch_width")
|
||||
|
||||
slider = pn.widgets.FloatSlider(start=0, end=10, name="amplitude")
|
||||
|
||||
|
||||
def callback(new):
|
||||
return f"Amplitude is: {new}"
|
||||
|
||||
|
||||
pn.Row(slider, pn.bind(callback, slider)).servable(target="simple_app")
|
||||
24
pyscript.core/tests/js-integration/async-listener.html
Normal file
24
pyscript.core/tests/js-integration/async-listener.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy">
|
||||
from pyscript import window, document, fetch, when
|
||||
|
||||
@when('click', '#click')
|
||||
async def click(event):
|
||||
text = await fetch(window.location.href).text()
|
||||
document.getElementById('output').append(text)
|
||||
document.documentElement.classList.add('ok')
|
||||
|
||||
document.getElementById('click').click()
|
||||
</script>
|
||||
<button id="click">click</button>
|
||||
<pre id="output"></pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,13 +4,21 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript Next Plugin</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<mpy-config src="config-url/config.json"></mpy-config>
|
||||
<script type="mpy">
|
||||
from pyscript import config
|
||||
if config["files"]["{TO}"] != "./runtime":
|
||||
raise Exception("wrong config tree")
|
||||
|
||||
from runtime import test
|
||||
</script>
|
||||
<script type="mpy" worker>
|
||||
from pyscript import config
|
||||
if config["files"]["{TO}"] != "./runtime":
|
||||
raise Exception("wrong config tree")
|
||||
|
||||
from runtime import test
|
||||
</script>
|
||||
</head>
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
@@ -18,7 +18,7 @@
|
||||
document.createElement('script'),
|
||||
{
|
||||
type: 'module',
|
||||
src: '../dist/core.js'
|
||||
src: '../../../dist/core.js'
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -4,8 +4,8 @@
|
||||
<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>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy">
|
||||
@@ -4,13 +4,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript Next Plugin Bug?</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module">
|
||||
addEventListener('mpy:done', () => {
|
||||
document.documentElement.classList.add('done');
|
||||
});
|
||||
|
||||
import { hooks } from "../dist/core.js";
|
||||
import { hooks } from "../../dist/core.js";
|
||||
|
||||
// Main
|
||||
hooks.main.onReady.add((wrap, element) => {
|
||||
@@ -48,12 +48,12 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" worker>
|
||||
<script type="mpy" async="false" worker>
|
||||
from pyscript import document
|
||||
print("actual code in worker")
|
||||
document.documentElement.classList.add('worker')
|
||||
</script>
|
||||
<script type="mpy">
|
||||
<script type="mpy" async="false">
|
||||
print("actual code in main")
|
||||
</script>
|
||||
</body>
|
||||
6
pyscript.core/tests/js-integration/issue-2093/error.js
Normal file
6
pyscript.core/tests/js-integration/issue-2093/error.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { error } = console;
|
||||
|
||||
console.error = (...args) => {
|
||||
error(...args);
|
||||
document.documentElement.classList.add('errored');
|
||||
};
|
||||
16
pyscript.core/tests/js-integration/issue-2093/index.html
Normal file
16
pyscript.core/tests/js-integration/issue-2093/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="./error.js"></script>
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy-editor" setup>
|
||||
print("Hello Editor")
|
||||
raise Exception("failure")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
28
pyscript.core/tests/js-integration/js-storage.html
Normal file
28
pyscript.core/tests/js-integration/js-storage.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module">
|
||||
import { storage } from '../../dist/storage.js';
|
||||
|
||||
const store = await storage('js-storage');
|
||||
store.test = [1, 2, 3].map(String);
|
||||
await store.sync();
|
||||
|
||||
// be sure the store is not empty before bootstrap
|
||||
import('../../dist/core.js');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy">
|
||||
from pyscript import storage, document
|
||||
|
||||
store = await storage("js-storage")
|
||||
|
||||
if ",".join(store["test"]) == "1,2,3":
|
||||
document.documentElement.classList.add("ok")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,8 +3,8 @@
|
||||
<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>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<mpy-config>
|
||||
29
pyscript.core/tests/js-integration/loader/index.html
Normal file
29
pyscript.core/tests/js-integration/loader/index.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module">
|
||||
import '../../../dist/core.js';
|
||||
|
||||
addEventListener('py:progress', ({ detail }) => {
|
||||
document.body.append(
|
||||
detail,
|
||||
document.createElement('br'),
|
||||
);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<py-config>
|
||||
packages = ["matplotlib"]
|
||||
</py-config>
|
||||
<script type="py" worker>
|
||||
from pyscript import document
|
||||
from sys import version
|
||||
document.body.append(document.createElement('hr'), version)
|
||||
document.documentElement.classList.add('ok')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,8 +9,8 @@
|
||||
document.documentElement.classList.add('done');
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy">
|
||||
@@ -4,8 +4,8 @@
|
||||
<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>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<style>.xterm { padding: .5rem; }</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -4,12 +4,12 @@
|
||||
<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>
|
||||
<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>
|
||||
<script type="mpy" src="terminal.py" worker terminal></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PyTerminal</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<style>.xterm { padding: .5rem; }</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -4,12 +4,12 @@
|
||||
<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>
|
||||
<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>
|
||||
<script type="mpy" worker terminal>
|
||||
from pyscript import document
|
||||
document.documentElement.classList.add("first")
|
||||
|
||||
46
pyscript.core/tests/js-integration/storage.html
Normal file
46
pyscript.core/tests/js-integration/storage.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@pyscript/core storage</title>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" async>
|
||||
from random import random
|
||||
from pyscript import storage
|
||||
|
||||
store = await storage(name="test")
|
||||
|
||||
print("before", len(store))
|
||||
for k in store:
|
||||
if isinstance(store[k], memoryview):
|
||||
print(f" {k}: {store[k].hex()} as hex()")
|
||||
else:
|
||||
print(f" {k}: {store[k]}")
|
||||
|
||||
store["ba"] = bytearray([0, 1, 2, 3, 4])
|
||||
store["mv"] = memoryview(bytearray([5, 6, 7, 8, 9]))
|
||||
store["random"] = ("some", random(), True)
|
||||
store["key"] = "value"
|
||||
|
||||
print("now", len(store))
|
||||
for k in store:
|
||||
print(f" {k}: {store[k]}")
|
||||
|
||||
del store["key"]
|
||||
# store.clear()
|
||||
|
||||
print("after", len(store))
|
||||
for k in store:
|
||||
print(f" {k}: {store[k]}")
|
||||
|
||||
await store.sync()
|
||||
|
||||
import js
|
||||
js.document.documentElement.classList.add("ok")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2
pyscript.core/tests/js-integration/workers/config.toml
Normal file
2
pyscript.core/tests/js-integration/workers/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[files]
|
||||
"./test.py" = "./test.py"
|
||||
30
pyscript.core/tests/js-integration/workers/index.html
Normal file
30
pyscript.core/tests/js-integration/workers/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" async>
|
||||
from pyscript import create_named_worker
|
||||
|
||||
await create_named_worker("./worker.py", name="micropython_version", type="mpy")
|
||||
</script>
|
||||
<script type="mpy" config="./config.toml" async>
|
||||
from test import test
|
||||
await test("mpy")
|
||||
</script>
|
||||
<script type="py" config="./config.toml" async>
|
||||
from test import test
|
||||
await test("py")
|
||||
</script>
|
||||
<script type="py" name="pyodide_version" worker>
|
||||
def pyodide_version():
|
||||
import sys
|
||||
return sys.version
|
||||
|
||||
__export__ = ['pyodide_version']
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
29
pyscript.core/tests/js-integration/workers/named.html
Normal file
29
pyscript.core/tests/js-integration/workers/named.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>named workers</title>
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" async>
|
||||
from pyscript import workers
|
||||
|
||||
await (await workers["mpy"]).greetings()
|
||||
await (await workers["py"]).greetings()
|
||||
</script>
|
||||
<script type="mpy" worker name="mpy">
|
||||
def greetings():
|
||||
print("micropython")
|
||||
|
||||
__export__ = ['greetings']
|
||||
</script>
|
||||
<script type="py" worker name="py">
|
||||
def greetings():
|
||||
print("pyodide")
|
||||
|
||||
__export__ = ['greetings']
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
19
pyscript.core/tests/js-integration/workers/test.py
Normal file
19
pyscript.core/tests/js-integration/workers/test.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from pyscript import document, workers
|
||||
|
||||
|
||||
async def test(interpreter):
|
||||
# accessed as item
|
||||
named = await workers.micropython_version
|
||||
|
||||
version = await named.micropython_version()
|
||||
document.body.append(version)
|
||||
document.body.append(document.createElement("hr"))
|
||||
|
||||
# accessed as attribute
|
||||
named = await workers["pyodide_version"]
|
||||
|
||||
version = await named.pyodide_version()
|
||||
document.body.append(version)
|
||||
document.body.append(document.createElement("hr"))
|
||||
|
||||
document.documentElement.classList.add(interpreter)
|
||||
7
pyscript.core/tests/js-integration/workers/worker.py
Normal file
7
pyscript.core/tests/js-integration/workers/worker.py
Normal file
@@ -0,0 +1,7 @@
|
||||
def micropython_version():
|
||||
import sys
|
||||
|
||||
return sys.version
|
||||
|
||||
|
||||
__export__ = ["micropython_version"]
|
||||
1
pyscript.core/tests/manual/a.py
Normal file
1
pyscript.core/tests/manual/a.py
Normal file
@@ -0,0 +1 @@
|
||||
print("a")
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module">
|
||||
import '../dist/core.js';
|
||||
import '../../dist/core.js';
|
||||
|
||||
document.body.append('loading ...', document.createElement('br'));
|
||||
|
||||
21
pyscript.core/tests/manual/async.html
Normal file
21
pyscript.core/tests/manual/async.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<py-script>
|
||||
import asyncio
|
||||
print('py-script sleep')
|
||||
await asyncio.sleep(1)
|
||||
print('py-script done')
|
||||
</py-script>
|
||||
<script type="py">
|
||||
import asyncio
|
||||
print('script-py sleep')
|
||||
await asyncio.sleep(1)
|
||||
print('script-py done')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,8 +4,8 @@
|
||||
<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>
|
||||
<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>
|
||||
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript Next Plugin Bug?</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py">
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module">
|
||||
import { hooks } from "../dist/core.js";
|
||||
import { hooks } from "../../dist/core.js";
|
||||
hooks.main.codeBeforeRun.add('print(0)');
|
||||
hooks.main.codeAfterRun.add('print(2)');
|
||||
</script>
|
||||
@@ -4,11 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PyScript Error</title>
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<py-config>
|
||||
[[fetch]]
|
||||
files = ["a.py"]
|
||||
files = ["./a.py"]
|
||||
</py-config>
|
||||
<script type="py" worker>
|
||||
import a
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user