mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1a0d8ea98 | ||
|
|
04222b0d03 | ||
|
|
8ec3381789 | ||
|
|
9bd4737708 | ||
|
|
c49cb9231b | ||
|
|
d1d1c5740f | ||
|
|
1a05ea5fd2 | ||
|
|
5b4e8527da | ||
|
|
83c2afeaf1 | ||
|
|
643b76479f | ||
|
|
cf92996071 | ||
|
|
c653296821 | ||
|
|
44cd6273ba | ||
|
|
d7d2dfb383 | ||
|
|
2d5cf096e0 | ||
|
|
6ee8217593 | ||
|
|
6d45728787 | ||
|
|
65954a627e | ||
|
|
2f1b764251 | ||
|
|
1fb6cddd70 | ||
|
|
239add4e20 | ||
|
|
4e4ac56729 | ||
|
|
1447cb3094 | ||
|
|
2f3659b676 | ||
|
|
910c666319 | ||
|
|
eee2f64c1d | ||
|
|
d080246a0f | ||
|
|
98c0f5e50d | ||
|
|
a1268f1aa2 | ||
|
|
69b8884045 | ||
|
|
df1d699fe6 |
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*" # Group all Actions updates into a single larger pull request
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
6
.github/workflows/prepare-release.yml
vendored
6
.github/workflows/prepare-release.yml
vendored
@@ -17,12 +17,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
run: zip -r -q ./build.zip ./dist
|
run: zip -r -q ./build.zip ./dist
|
||||||
|
|
||||||
- name: Prepare Release
|
- name: Prepare Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
4
.github/workflows/publish-release.yml
vendored
4
.github/workflows/publish-release.yml
vendored
@@ -19,12 +19,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/publish-snapshot.yml
vendored
4
.github/workflows/publish-snapshot.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/publish-unstable.yml
vendored
4
.github/workflows/publish-unstable.yml
vendored
@@ -24,12 +24,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
|
|||||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -37,12 +37,12 @@ jobs:
|
|||||||
run: git log --graph -3
|
run: git log --graph -3
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: setup Miniconda
|
- name: setup Miniconda
|
||||||
uses: conda-incubator/setup-miniconda@v2
|
uses: conda-incubator/setup-miniconda@v3
|
||||||
|
|
||||||
- name: Create and activate virtual environment
|
- name: Create and activate virtual environment
|
||||||
run: |
|
run: |
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
make test-integration
|
make test-integration
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: pyscript
|
name: pyscript
|
||||||
path: |
|
path: |
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
with:
|
with:
|
||||||
name: test_results
|
name: test_results
|
||||||
|
|||||||
2
.github/workflows/test_report.yml
vendored
2
.github/workflows/test_report.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
report:
|
report:
|
||||||
runs-on: ubuntu-latest-8core
|
runs-on: ubuntu-latest-8core
|
||||||
steps:
|
steps:
|
||||||
- uses: dorny/test-reporter@v1.6.0
|
- uses: dorny/test-reporter@v1.9.0
|
||||||
with:
|
with:
|
||||||
artifact: test_results
|
artifact: test_results
|
||||||
name: Test reports
|
name: Test reports
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ ci:
|
|||||||
default_stages: [commit]
|
default_stages: [commit]
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-builtin-literals
|
- id: check-builtin-literals
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
@@ -25,7 +25,7 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 24.1.1
|
rev: 24.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
|
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ node_modules/
|
|||||||
rollup/
|
rollup/
|
||||||
test/
|
test/
|
||||||
tests/
|
tests/
|
||||||
|
test-results/
|
||||||
src/stdlib/_pyscript
|
src/stdlib/_pyscript
|
||||||
src/stdlib/pyscript.py
|
src/stdlib/pyscript.py
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|||||||
652
pyscript.core/package-lock.json
generated
652
pyscript.core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pyscript/core",
|
"name": "@pyscript/core",
|
||||||
"version": "0.4.5",
|
"version": "0.4.32",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "PyScript",
|
"description": "PyScript",
|
||||||
"module": "./index.js",
|
"module": "./index.js",
|
||||||
@@ -20,13 +20,14 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"server": "npx static-handler --coi .",
|
"server": "npx static-handler --coi .",
|
||||||
"build": "npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy",
|
"build": "export ESLINT_USE_FLAT_CONFIG=false; npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy",
|
||||||
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
||||||
"build:plugins": "node rollup/plugins.cjs",
|
"build:plugins": "node rollup/plugins.cjs",
|
||||||
"build:stdlib": "node rollup/stdlib.cjs",
|
"build:stdlib": "node rollup/stdlib.cjs",
|
||||||
"build:3rd-party": "node rollup/3rd-party.cjs",
|
"build:3rd-party": "node rollup/3rd-party.cjs",
|
||||||
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||||
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/ || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/mpy.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||||
|
"test:ws": "bun test/ws/index.js & playwright test test/ws.spec.js",
|
||||||
"dev": "node dev.cjs",
|
"dev": "node dev.cjs",
|
||||||
"release": "npm run build && npm run zip",
|
"release": "npm run build && npm run zip",
|
||||||
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
|
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
|
||||||
@@ -42,31 +43,33 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ungap/with-resolvers": "^0.1.0",
|
"@ungap/with-resolvers": "^0.1.0",
|
||||||
"basic-devtools": "^0.1.6",
|
"basic-devtools": "^0.1.6",
|
||||||
"polyscript": "^0.9.0",
|
"polyscript": "^0.12.8",
|
||||||
"sticky-module": "^0.1.1",
|
"sticky-module": "^0.1.1",
|
||||||
"to-json-callback": "^0.1.1",
|
"to-json-callback": "^0.1.1",
|
||||||
"type-checked-collections": "^0.1.7"
|
"type-checked-collections": "^0.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/commands": "^6.3.3",
|
"@codemirror/commands": "^6.5.0",
|
||||||
"@codemirror/lang-python": "^6.1.4",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
"@codemirror/state": "^6.4.0",
|
"@codemirror/state": "^6.4.1",
|
||||||
"@codemirror/view": "^6.24.0",
|
"@codemirror/view": "^6.26.3",
|
||||||
"@playwright/test": "^1.41.2",
|
"@playwright/test": "^1.44.0",
|
||||||
"@rollup/plugin-commonjs": "^25.0.7",
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@webreflection/toml-j0.4": "^1.1.3",
|
"@webreflection/toml-j0.4": "^1.1.3",
|
||||||
"@xterm/addon-fit": "^0.9.0-beta.1",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
|
"bun": "^1.1.7",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^9.2.0",
|
||||||
"rollup": "^4.11.0",
|
"rollup": "^4.17.2",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-string": "^3.0.0",
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"static-handler": "^0.4.3",
|
"static-handler": "^0.4.3",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.4.5",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-readline": "^1.1.1"
|
"xterm-readline": "^1.1.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ const modules = {
|
|||||||
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||||
b.text(),
|
b.text(),
|
||||||
),
|
),
|
||||||
|
"xterm_addon-web-links.js": fetch(
|
||||||
|
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||||
|
).then((b) => b.text()),
|
||||||
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
||||||
(b) => b.text(),
|
(b) => b.text(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ import { ErrorCode } from "./exceptions.js";
|
|||||||
import { robustFetch as fetch, getText } from "./fetch.js";
|
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||||
import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
|
import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
|
||||||
|
|
||||||
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
|
export { stdlib, optional };
|
||||||
|
|
||||||
// generic helper to disambiguate between custom element and script
|
// generic helper to disambiguate between custom element and script
|
||||||
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
||||||
|
|
||||||
@@ -81,6 +84,9 @@ export {
|
|||||||
exportedWhenDefined as whenDefined,
|
exportedWhenDefined as whenDefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const offline_interpreter = (config) =>
|
||||||
|
config?.interpreter && new URL(config.interpreter, location.href).href;
|
||||||
|
|
||||||
const hooked = new Map();
|
const hooked = new Map();
|
||||||
|
|
||||||
for (const [TYPE, interpreter] of TYPES) {
|
for (const [TYPE, interpreter] of TYPES) {
|
||||||
@@ -165,7 +171,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
// specific main and worker hooks
|
// specific main and worker hooks
|
||||||
const hooks = {
|
const hooks = {
|
||||||
main: {
|
main: {
|
||||||
...codeFor(main),
|
...codeFor(main, TYPE),
|
||||||
async onReady(wrap, element) {
|
async onReady(wrap, element) {
|
||||||
registerModule(wrap);
|
registerModule(wrap);
|
||||||
|
|
||||||
@@ -262,7 +268,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
worker: {
|
worker: {
|
||||||
...codeFor(worker),
|
...codeFor(worker, TYPE),
|
||||||
// these are lazy getters that returns a composition
|
// these are lazy getters that returns a composition
|
||||||
// of the current hooks or undefined, if no hook is present
|
// of the current hooks or undefined, if no hook is present
|
||||||
get onReady() {
|
get onReady() {
|
||||||
@@ -291,7 +297,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
interpreter,
|
interpreter,
|
||||||
hooks,
|
hooks,
|
||||||
env: `${TYPE}-script`,
|
env: `${TYPE}-script`,
|
||||||
version: config?.interpreter,
|
version: offline_interpreter(config),
|
||||||
onerror(error, element) {
|
onerror(error, element) {
|
||||||
errors.set(element, error);
|
errors.set(element, error);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { typedSet } from "type-checked-collections";
|
|||||||
import { dedent } from "polyscript/exports";
|
import { dedent } from "polyscript/exports";
|
||||||
import toJSONCallback from "to-json-callback";
|
import toJSONCallback from "to-json-callback";
|
||||||
|
|
||||||
import stdlib from "./stdlib.js";
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
|
|
||||||
export const main = (name) => hooks.main[name];
|
export const main = (name) => hooks.main[name];
|
||||||
export const worker = (name) => hooks.worker[name];
|
export const worker = (name) => hooks.worker[name];
|
||||||
@@ -15,10 +15,11 @@ const code = (hooks, branch, key, lib) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const codeFor = (branch) => {
|
export const codeFor = (branch, type) => {
|
||||||
|
const pylib = type === "mpy" ? stdlib.replace(optional, "") : stdlib;
|
||||||
const hooks = {};
|
const hooks = {};
|
||||||
code(hooks, branch, `codeBeforeRun`, stdlib);
|
code(hooks, branch, `codeBeforeRun`, pylib);
|
||||||
code(hooks, branch, `codeBeforeRunAsync`, stdlib);
|
code(hooks, branch, `codeBeforeRunAsync`, pylib);
|
||||||
code(hooks, branch, `codeAfterRun`);
|
code(hooks, branch, `codeAfterRun`);
|
||||||
code(hooks, branch, `codeAfterRunAsync`);
|
code(hooks, branch, `codeAfterRunAsync`);
|
||||||
return hooks;
|
return hooks;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// PyScript py-editor plugin
|
// PyScript py-editor plugin
|
||||||
import { Hook, XWorker, dedent } from "polyscript/exports";
|
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||||
import { TYPES } from "../core.js";
|
import { TYPES, offline_interpreter, stdlib } from "../core.js";
|
||||||
|
|
||||||
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
||||||
|
|
||||||
@@ -8,13 +8,15 @@ let id = 0;
|
|||||||
const getID = (type) => `${type}-editor-${id++}`;
|
const getID = (type) => `${type}-editor-${id++}`;
|
||||||
|
|
||||||
const envs = new Map();
|
const envs = new Map();
|
||||||
|
const configs = new Map();
|
||||||
|
|
||||||
const hooks = {
|
const hooks = {
|
||||||
worker: {
|
worker: {
|
||||||
|
codeBeforeRun: () => stdlib,
|
||||||
// works on both Pyodide and MicroPython
|
// works on both Pyodide and MicroPython
|
||||||
onReady: ({ runAsync, io }, { sync }) => {
|
onReady: ({ runAsync, io }, { sync }) => {
|
||||||
io.stdout = (line) => sync.write(line);
|
io.stdout = io.buffered(sync.write);
|
||||||
io.stderr = (line) => sync.writeErr(line);
|
io.stderr = io.buffered(sync.writeErr);
|
||||||
sync.revoke();
|
sync.revoke();
|
||||||
sync.runAsync = runAsync;
|
sync.runAsync = runAsync;
|
||||||
},
|
},
|
||||||
@@ -23,15 +25,29 @@ const hooks = {
|
|||||||
|
|
||||||
async function execute({ currentTarget }) {
|
async function execute({ currentTarget }) {
|
||||||
const { env, pySrc, outDiv } = this;
|
const { env, pySrc, outDiv } = this;
|
||||||
|
const hasRunButton = !!currentTarget;
|
||||||
|
|
||||||
currentTarget.disabled = true;
|
if (hasRunButton) {
|
||||||
outDiv.innerHTML = "";
|
currentTarget.disabled = true;
|
||||||
|
outDiv.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
if (!envs.has(env)) {
|
if (!envs.has(env)) {
|
||||||
const srcLink = URL.createObjectURL(new Blob([""]));
|
const srcLink = URL.createObjectURL(new Blob([""]));
|
||||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, {
|
const details = { type: this.interpreter };
|
||||||
type: this.interpreter,
|
const { config } = this;
|
||||||
});
|
if (config) {
|
||||||
|
details.configURL = config;
|
||||||
|
const { parse } = config.endsWith(".toml")
|
||||||
|
? await import(/* webpackIgnore: true */ "../3rd-party/toml.js")
|
||||||
|
: JSON;
|
||||||
|
details.config = parse(await fetch(config).then((r) => r.text()));
|
||||||
|
details.version = offline_interpreter(details.config);
|
||||||
|
} else {
|
||||||
|
details.config = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||||
|
|
||||||
const { sync } = xworker;
|
const { sync } = xworker;
|
||||||
const { promise, resolve } = Promise.withResolvers();
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
@@ -44,23 +60,27 @@ async function execute({ currentTarget }) {
|
|||||||
|
|
||||||
// wait for the env then set the target div
|
// wait for the env then set the target div
|
||||||
// before executing the current code
|
// before executing the current code
|
||||||
envs.get(env).then((xworker) => {
|
return envs.get(env).then((xworker) => {
|
||||||
xworker.onerror = ({ error }) => {
|
xworker.onerror = ({ error }) => {
|
||||||
outDiv.innerHTML += `<span style='color:red'>${
|
if (hasRunButton) {
|
||||||
error.message || error
|
outDiv.innerHTML += `<span style='color:red'>${
|
||||||
}</span>\n`;
|
error.message || error
|
||||||
|
}</span>\n`;
|
||||||
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
const enable = () => {
|
const enable = () => {
|
||||||
currentTarget.disabled = false;
|
if (hasRunButton) currentTarget.disabled = false;
|
||||||
};
|
};
|
||||||
const { sync } = xworker;
|
const { sync } = xworker;
|
||||||
sync.write = (str) => {
|
sync.write = (str) => {
|
||||||
outDiv.innerText += `${str}\n`;
|
if (hasRunButton) outDiv.innerText += `${str}\n`;
|
||||||
};
|
};
|
||||||
sync.writeErr = (str) => {
|
sync.writeErr = (str) => {
|
||||||
outDiv.innerHTML += `<span style='color:red'>${str}</span>\n`;
|
if (hasRunButton) {
|
||||||
|
outDiv.innerHTML += `<span style='color:red'>${str}</span>\n`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
sync.runAsync(pySrc).then(enable, enable);
|
sync.runAsync(pySrc).then(enable, enable);
|
||||||
});
|
});
|
||||||
@@ -120,7 +140,6 @@ const init = async (script, type, interpreter) => {
|
|||||||
{ keymap },
|
{ keymap },
|
||||||
{ defaultKeymap },
|
{ defaultKeymap },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
// TODO: find a way to actually produce these bundles locally
|
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
||||||
import(
|
import(
|
||||||
@@ -131,9 +150,75 @@ const init = async (script, type, interpreter) => {
|
|||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const selector = script.getAttribute("target");
|
let isSetup = script.hasAttribute("setup");
|
||||||
|
const hasConfig = script.hasAttribute("config");
|
||||||
|
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
|
||||||
|
|
||||||
|
if (hasConfig && configs.has(env)) {
|
||||||
|
throw new SyntaxError(
|
||||||
|
configs.get(env)
|
||||||
|
? `duplicated config for env: ${env}`
|
||||||
|
: `unable to add a config to the env: ${env}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
configs.set(env, hasConfig);
|
||||||
|
|
||||||
|
let source = script.src
|
||||||
|
? await fetch(script.src).then((b) => b.text())
|
||||||
|
: script.textContent;
|
||||||
|
const context = {
|
||||||
|
interpreter,
|
||||||
|
env,
|
||||||
|
config:
|
||||||
|
hasConfig &&
|
||||||
|
new URL(script.getAttribute("config"), location.href).href,
|
||||||
|
get pySrc() {
|
||||||
|
return isSetup ? source : editor.state.doc.toString();
|
||||||
|
},
|
||||||
|
get outDiv() {
|
||||||
|
return isSetup ? null : outDiv;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let target;
|
let target;
|
||||||
|
defineProperties(script, {
|
||||||
|
target: { get: () => target },
|
||||||
|
process: {
|
||||||
|
/**
|
||||||
|
* Simulate a setup node overriding the source to evaluate.
|
||||||
|
* @param {string} code the Python code to evaluate.
|
||||||
|
* @returns {Promise<...>} fulfill once code has been evaluated.
|
||||||
|
*/
|
||||||
|
value(code) {
|
||||||
|
const wasSetup = isSetup;
|
||||||
|
const wasSource = source;
|
||||||
|
isSetup = true;
|
||||||
|
source = code;
|
||||||
|
const restore = () => {
|
||||||
|
isSetup = wasSetup;
|
||||||
|
source = wasSource;
|
||||||
|
};
|
||||||
|
return execute
|
||||||
|
.call(context, { currentTarget: null })
|
||||||
|
.then(restore, restore);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const notify = () => {
|
||||||
|
const event = new Event(`${type}-editor`, { bubbles: true });
|
||||||
|
script.dispatchEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSetup) {
|
||||||
|
await execute.call(context, { currentTarget: null });
|
||||||
|
notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selector = script.getAttribute("target");
|
||||||
|
|
||||||
if (selector) {
|
if (selector) {
|
||||||
target =
|
target =
|
||||||
document.getElementById(selector) ||
|
document.getElementById(selector) ||
|
||||||
@@ -149,18 +234,6 @@ const init = async (script, type, interpreter) => {
|
|||||||
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
|
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
|
||||||
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
|
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
|
||||||
|
|
||||||
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
|
|
||||||
const context = {
|
|
||||||
interpreter,
|
|
||||||
env,
|
|
||||||
get pySrc() {
|
|
||||||
return editor.state.doc.toString();
|
|
||||||
},
|
|
||||||
get outDiv() {
|
|
||||||
return outDiv;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
|
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
|
||||||
const listener = execute.bind(context);
|
const listener = execute.bind(context);
|
||||||
const [boxDiv, outDiv] = makeBoxDiv(listener, type);
|
const [boxDiv, outDiv] = makeBoxDiv(listener, type);
|
||||||
@@ -195,6 +268,7 @@ const init = async (script, type, interpreter) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
editor.focus();
|
editor.focus();
|
||||||
|
notify();
|
||||||
};
|
};
|
||||||
|
|
||||||
// avoid too greedy MutationObserver operations at distance
|
// avoid too greedy MutationObserver operations at distance
|
||||||
@@ -210,7 +284,7 @@ const resetTimeout = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// triggered both ASAP on the living DOM and via MutationObserver later
|
// triggered both ASAP on the living DOM and via MutationObserver later
|
||||||
const pyEditor = async () => {
|
const pyEditor = () => {
|
||||||
if (timeout) return;
|
if (timeout) return;
|
||||||
timeout = setTimeout(resetTimeout, 250);
|
timeout = setTimeout(resetTimeout, 250);
|
||||||
for (const [type, interpreter] of TYPES) {
|
for (const [type, interpreter] of TYPES) {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// PyScript py-terminal plugin
|
// PyScript py-terminal plugin
|
||||||
import { TYPES, hooks } from "../core.js";
|
import { TYPES, hooks } from "../core.js";
|
||||||
import { notify } from "./error.js";
|
import { notify } from "./error.js";
|
||||||
import { defineProperty } from "polyscript/exports";
|
import { customObserver, defineProperties } from "polyscript/exports";
|
||||||
|
|
||||||
const SELECTOR = [...TYPES.keys()]
|
// will contain all valid selectors
|
||||||
.map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`)
|
const SELECTORS = [];
|
||||||
.join(",");
|
|
||||||
|
|
||||||
// show the error on main and
|
// show the error on main and
|
||||||
// stops the module from keep executing
|
// stops the module from keep executing
|
||||||
@@ -14,8 +13,6 @@ const notifyAndThrow = (message) => {
|
|||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const notParsedYet = (script) => !bootstrapped.has(script);
|
|
||||||
|
|
||||||
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
||||||
|
|
||||||
const bootstrapped = new WeakSet();
|
const bootstrapped = new WeakSet();
|
||||||
@@ -25,182 +22,276 @@ let addStyle = true;
|
|||||||
// this callback will be serialized as string and it never needs
|
// this callback will be serialized as string and it never needs
|
||||||
// to be invoked multiple times. Each xworker here is bootstrapped
|
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||||
// only once thanks to the `sync.is_pyterminal()` check.
|
// only once thanks to the `sync.is_pyterminal()` check.
|
||||||
const workerReady = ({ interpreter, io, run }, { sync }) => {
|
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||||
if (!sync.is_pyterminal()) return;
|
if (!sync.is_pyterminal()) return;
|
||||||
|
|
||||||
// in workers it's always safe to grab the polyscript currentScript
|
// in workers it's always safe to grab the polyscript currentScript
|
||||||
run("from polyscript.currentScript import terminal as __terminal__");
|
// 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 _",
|
||||||
|
);
|
||||||
|
|
||||||
// This part is inevitably duplicated as external scope
|
|
||||||
// can't be reached by workers out of the box.
|
|
||||||
// The detail is that here we use sync though, not readline.
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let data = "";
|
let data = "";
|
||||||
|
const { pyterminal_read, pyterminal_write } = sync;
|
||||||
|
const decoder = new TextDecoder();
|
||||||
const generic = {
|
const generic = {
|
||||||
isatty: true,
|
isatty: false,
|
||||||
write(buffer) {
|
write(buffer) {
|
||||||
data = decoder.decode(buffer);
|
data = decoder.decode(buffer);
|
||||||
sync.pyterminal_write(data);
|
pyterminal_write(data);
|
||||||
return buffer.length;
|
return buffer.length;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
interpreter.setStdout(generic);
|
|
||||||
interpreter.setStderr(generic);
|
|
||||||
interpreter.setStdin({
|
|
||||||
isatty: true,
|
|
||||||
stdin: () => sync.pyterminal_read(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// This part works already in both Pyodide and MicroPython
|
||||||
io.stderr = (error) => {
|
io.stderr = (error) => {
|
||||||
sync.pyterminal_write(`${error.message || error}\n`);
|
pyterminal_write(String(error.message || error));
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const pyTerminal = async () => {
|
// MicroPython has no code or code.interact()
|
||||||
const terminals = document.querySelectorAll(SELECTOR);
|
// 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");
|
||||||
|
|
||||||
const unknown = [].filter.call(terminals, notParsedYet);
|
// this is needed to avoid truncated unicode in MicroPython
|
||||||
|
// the reason is that `linebuffer` false just send one byte
|
||||||
// no results will look further for runtime nodes
|
// per time and readline here doesn't like it much.
|
||||||
if (!unknown.length) return;
|
// MicroPython also has issues with code-points and
|
||||||
// early flag elements as known to avoid concurrent
|
// replProcessChar(byte) but that function accepts only
|
||||||
// MutationObserver invokes of this async handler
|
// one byte per time so ... we have an issue!
|
||||||
else unknown.forEach(bootstrapped.add, bootstrapped);
|
// @see https://github.com/pyscript/pyscript/pull/2018
|
||||||
|
// @see https://github.com/WebReflection/buffer-points
|
||||||
// we currently support only one terminal as in "classic"
|
const bufferPoints = (stdio) => {
|
||||||
if ([].filter.call(terminals, onceOnMain).length > 1)
|
const bytes = [];
|
||||||
notifyAndThrow("You can use at most 1 main terminal");
|
let needed = 0;
|
||||||
|
return (buffer) => {
|
||||||
// import styles lazily
|
let written = 0;
|
||||||
if (addStyle) {
|
for (const byte of buffer) {
|
||||||
addStyle = false;
|
bytes.push(byte);
|
||||||
document.head.append(
|
// @see https://encoding.spec.whatwg.org/#utf-8-bytes-needed
|
||||||
Object.assign(document.createElement("link"), {
|
if (needed) needed--;
|
||||||
rel: "stylesheet",
|
else if (0xc2 <= byte && byte <= 0xdf) needed = 1;
|
||||||
href: new URL("./xterm.css", import.meta.url),
|
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)));
|
||||||
// lazy load these only when a valid terminal is found
|
}
|
||||||
const [{ Terminal }, { Readline }, { FitAddon }] = await Promise.all([
|
}
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/xterm.js"),
|
return written;
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/xterm-readline.js"),
|
};
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (const element of unknown) {
|
|
||||||
// hopefully to be removed in the near future!
|
|
||||||
if (element.matches('script[type="mpy"],mpy-script'))
|
|
||||||
notifyAndThrow("Unsupported terminal.");
|
|
||||||
|
|
||||||
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.open(target);
|
|
||||||
fitAddon.fit();
|
|
||||||
terminal.focus();
|
|
||||||
defineProperty(element, "terminal", { value: terminal });
|
|
||||||
return terminal;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// branch logic for the worker
|
io.stdout = bufferPoints(generic.write);
|
||||||
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
|
// tiny shim of the code module with only interact
|
||||||
hooks.main.onWorker.delete(worker);
|
// to bootstrap a REPL like environment
|
||||||
|
interpreter.registerJsModule("code", {
|
||||||
|
interact() {
|
||||||
|
let input = "";
|
||||||
|
let length = 1;
|
||||||
|
|
||||||
init({
|
const encoder = new TextEncoder();
|
||||||
disableStdin: false,
|
const acc = [];
|
||||||
cursorBlink: true,
|
const handlePoints = bufferPoints((buffer) => {
|
||||||
cursorStyle: "block",
|
acc.push(...buffer);
|
||||||
|
pyterminal_write(decoder.decode(buffer));
|
||||||
});
|
});
|
||||||
|
|
||||||
xworker.sync.is_pyterminal = () => true;
|
// avoid duplicating the output produced by the input
|
||||||
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
io.stdout = (buffer) =>
|
||||||
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
length++ > input.length ? handlePoints(buffer) : 0;
|
||||||
});
|
|
||||||
|
|
||||||
// setup remote thread JS/Python code for whenever the
|
interpreter.replInit();
|
||||||
// worker is ready to become a terminal
|
|
||||||
hooks.worker.onReady.add(workerReady);
|
|
||||||
} else {
|
|
||||||
// in the main case, just bootstrap XTerm without
|
|
||||||
// allowing any input as that's not possible / awkward
|
|
||||||
hooks.main.onReady.add(function main({ interpreter, io, run }) {
|
|
||||||
console.warn("py-terminal is read only on main thread");
|
|
||||||
hooks.main.onReady.delete(main);
|
|
||||||
|
|
||||||
// on main, it's easy to trash and clean the current terminal
|
// loop forever waiting for user inputs
|
||||||
globalThis.__py_terminal__ = init({
|
(function repl() {
|
||||||
disableStdin: true,
|
const out = decoder.decode(new Uint8Array(acc.splice(0)));
|
||||||
cursorBlink: false,
|
// print in current line only the last line produced by the REPL
|
||||||
cursorStyle: "underline",
|
const data = `${pyterminal_read(out.split("\n").at(-1))}\r`;
|
||||||
});
|
length = 0;
|
||||||
run("from js import __py_terminal__ as __terminal__");
|
input = encoder.encode(data);
|
||||||
delete globalThis.__py_terminal__;
|
for (const c of input) interpreter.replProcessChar(c);
|
||||||
|
repl();
|
||||||
// This part is inevitably duplicated as external scope
|
})();
|
||||||
// can't be reached by workers out of the box.
|
},
|
||||||
// The detail is that here we use readline here, not sync.
|
});
|
||||||
const decoder = new TextDecoder();
|
} else {
|
||||||
let data = "";
|
interpreter.setStdout(generic);
|
||||||
const generic = {
|
interpreter.setStderr(generic);
|
||||||
isatty: true,
|
interpreter.setStdin({
|
||||||
write(buffer) {
|
isatty: false,
|
||||||
data = decoder.decode(buffer);
|
stdin: () => pyterminal_read(data),
|
||||||
readline.write(data);
|
});
|
||||||
return buffer.length;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
interpreter.setStdout(generic);
|
|
||||||
interpreter.setStderr(generic);
|
|
||||||
interpreter.setStdin({
|
|
||||||
isatty: true,
|
|
||||||
stdin: () => readline.read(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
io.stderr = (error) => {
|
|
||||||
readline.write(`${error.message || error}\n`);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mo = new MutationObserver(pyTerminal);
|
const pyTerminal = async (element) => {
|
||||||
mo.observe(document, { childList: true, subtree: true });
|
// 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"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
// try to check the current document ASAP
|
const readline = new Readline();
|
||||||
export default pyTerminal();
|
|
||||||
|
// 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]`;
|
||||||
|
SELECTORS.push(selector);
|
||||||
|
customObserver.set(selector, async (element) => {
|
||||||
|
// we currently support only one terminal on main as in "classic"
|
||||||
|
const terminals = document.querySelectorAll(SELECTORS.join(","));
|
||||||
|
if ([].filter.call(terminals, onceOnMain).length > 1)
|
||||||
|
notifyAndThrow("You can use at most 1 main terminal");
|
||||||
|
|
||||||
|
// import styles lazily
|
||||||
|
if (addStyle) {
|
||||||
|
addStyle = false;
|
||||||
|
document.head.append(
|
||||||
|
Object.assign(document.createElement("link"), {
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: new URL("./xterm.css", import.meta.url),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await pyTerminal(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,27 @@
|
|||||||
|
|
||||||
import pyscript from "./stdlib/pyscript.js";
|
import pyscript from "./stdlib/pyscript.js";
|
||||||
|
|
||||||
|
class Ignore extends Array {
|
||||||
|
#add = false;
|
||||||
|
#paths;
|
||||||
|
#array;
|
||||||
|
constructor(array, ...paths) {
|
||||||
|
super();
|
||||||
|
this.#array = array;
|
||||||
|
this.#paths = paths;
|
||||||
|
}
|
||||||
|
push(...values) {
|
||||||
|
if (this.#add) super.push(...values);
|
||||||
|
return this.#array.push(...values);
|
||||||
|
}
|
||||||
|
path(path) {
|
||||||
|
for (const _path of this.#paths) {
|
||||||
|
// bails out at the first `true` value
|
||||||
|
if ((this.#add = path.startsWith(_path))) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { entries } = Object;
|
const { entries } = Object;
|
||||||
|
|
||||||
const python = [
|
const python = [
|
||||||
@@ -16,16 +37,19 @@ const python = [
|
|||||||
"_path = None",
|
"_path = None",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ignore = new Ignore(python, "./pyweb");
|
||||||
|
|
||||||
const write = (base, literal) => {
|
const write = (base, literal) => {
|
||||||
for (const [key, value] of entries(literal)) {
|
for (const [key, value] of entries(literal)) {
|
||||||
python.push(`_path = _Path("${base}/${key}")`);
|
ignore.path(`${base}/${key}`);
|
||||||
|
ignore.push(`_path = _Path("${base}/${key}")`);
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
const code = JSON.stringify(value);
|
const code = JSON.stringify(value);
|
||||||
python.push(`_path.write_text(${code},encoding="utf-8")`);
|
ignore.push(`_path.write_text(${code},encoding="utf-8")`);
|
||||||
} else {
|
} else {
|
||||||
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
|
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
|
||||||
python.push(`if not _os.path.exists("${base}/${key}"):`);
|
ignore.push(`if not _os.path.exists("${base}/${key}"):`);
|
||||||
python.push(" _path.mkdir(parents=True, exist_ok=True)");
|
ignore.push(" _path.mkdir(parents=True, exist_ok=True)");
|
||||||
write(`${base}/${key}`, value);
|
write(`${base}/${key}`, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,4 +66,5 @@ python.push(
|
|||||||
);
|
);
|
||||||
python.push("\n");
|
python.push("\n");
|
||||||
|
|
||||||
export default python.join("\n");
|
export const stdlib = python.join("\n");
|
||||||
|
export const optional = ignore.join("\n");
|
||||||
|
|||||||
@@ -30,15 +30,18 @@
|
|||||||
# as it works transparently in both the main thread and worker cases.
|
# as it works transparently in both the main thread and worker cases.
|
||||||
|
|
||||||
from pyscript.display import HTML, display
|
from pyscript.display import HTML, display
|
||||||
|
from pyscript.fetch import fetch
|
||||||
from pyscript.magic_js import (
|
from pyscript.magic_js import (
|
||||||
RUNNING_IN_WORKER,
|
RUNNING_IN_WORKER,
|
||||||
PyWorker,
|
PyWorker,
|
||||||
|
config,
|
||||||
current_target,
|
current_target,
|
||||||
document,
|
document,
|
||||||
js_modules,
|
js_modules,
|
||||||
sync,
|
sync,
|
||||||
window,
|
window,
|
||||||
)
|
)
|
||||||
|
from pyscript.websocket import WebSocket
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pyscript.event_handling import when
|
from pyscript.event_handling import when
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import re
|
|||||||
from pyscript.magic_js import current_target, document, window
|
from pyscript.magic_js import current_target, document, window
|
||||||
|
|
||||||
_MIME_METHODS = {
|
_MIME_METHODS = {
|
||||||
"__repr__": "text/plain",
|
|
||||||
"_repr_html_": "text/html",
|
|
||||||
"_repr_markdown_": "text/markdown",
|
|
||||||
"_repr_svg_": "image/svg+xml",
|
|
||||||
"_repr_pdf_": "application/pdf",
|
|
||||||
"_repr_jpeg_": "image/jpeg",
|
|
||||||
"_repr_png_": "image/png",
|
|
||||||
"_repr_latex": "text/latex",
|
|
||||||
"_repr_json_": "application/json",
|
|
||||||
"_repr_javascript_": "application/javascript",
|
|
||||||
"savefig": "image/png",
|
"savefig": "image/png",
|
||||||
|
"_repr_javascript_": "application/javascript",
|
||||||
|
"_repr_json_": "application/json",
|
||||||
|
"_repr_latex": "text/latex",
|
||||||
|
"_repr_png_": "image/png",
|
||||||
|
"_repr_jpeg_": "image/jpeg",
|
||||||
|
"_repr_pdf_": "application/pdf",
|
||||||
|
"_repr_svg_": "image/svg+xml",
|
||||||
|
"_repr_markdown_": "text/markdown",
|
||||||
|
"_repr_html_": "text/html",
|
||||||
|
"__repr__": "text/plain",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ def _format_mime(obj):
|
|||||||
format_dict = mimebundle
|
format_dict = mimebundle
|
||||||
|
|
||||||
output, not_available = None, []
|
output, not_available = None, []
|
||||||
for method, mime_type in reversed(_MIME_METHODS.items()):
|
for method, mime_type in _MIME_METHODS.items():
|
||||||
if mime_type in format_dict:
|
if mime_type in format_dict:
|
||||||
output = format_dict[mime_type]
|
output = format_dict[mime_type]
|
||||||
else:
|
else:
|
||||||
|
|||||||
87
pyscript.core/src/stdlib/pyscript/fetch.py
Normal file
87
pyscript.core/src/stdlib/pyscript/fetch.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import js
|
||||||
|
from pyscript.util import as_bytearray
|
||||||
|
|
||||||
|
|
||||||
|
### wrap the response to grant Pythonic results
|
||||||
|
class _Response:
|
||||||
|
def __init__(self, response):
|
||||||
|
self._response = response
|
||||||
|
|
||||||
|
# grant access to response.ok and other fields
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self._response, attr)
|
||||||
|
|
||||||
|
# exposed methods with Pythonic results
|
||||||
|
async def arrayBuffer(self):
|
||||||
|
buffer = await self._response.arrayBuffer()
|
||||||
|
# works in Pyodide
|
||||||
|
if hasattr(buffer, "to_py"):
|
||||||
|
return buffer.to_py()
|
||||||
|
# shims in MicroPython
|
||||||
|
return memoryview(as_bytearray(buffer))
|
||||||
|
|
||||||
|
async def blob(self):
|
||||||
|
return await self._response.blob()
|
||||||
|
|
||||||
|
async def bytearray(self):
|
||||||
|
buffer = await self._response.arrayBuffer()
|
||||||
|
return as_bytearray(buffer)
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
return json.loads(await self.text())
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
return await self._response.text()
|
||||||
|
|
||||||
|
|
||||||
|
### allow direct await to _Response methods
|
||||||
|
class _DirectResponse:
|
||||||
|
@staticmethod
|
||||||
|
def setup(promise, response):
|
||||||
|
promise._response = _Response(response)
|
||||||
|
return promise._response
|
||||||
|
|
||||||
|
def __init__(self, promise):
|
||||||
|
self._promise = promise
|
||||||
|
promise._response = None
|
||||||
|
promise.arrayBuffer = self.arrayBuffer
|
||||||
|
promise.blob = self.blob
|
||||||
|
promise.bytearray = self.bytearray
|
||||||
|
promise.json = self.json
|
||||||
|
promise.text = self.text
|
||||||
|
|
||||||
|
async def _response(self):
|
||||||
|
if not self._promise._response:
|
||||||
|
await self._promise
|
||||||
|
return self._promise._response
|
||||||
|
|
||||||
|
async def arrayBuffer(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.arrayBuffer()
|
||||||
|
|
||||||
|
async def blob(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.blob()
|
||||||
|
|
||||||
|
async def bytearray(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.bytearray()
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.text()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url, **kw):
|
||||||
|
# workaround Pyodide / MicroPython dict <-> js conversion
|
||||||
|
options = js.JSON.parse(json.dumps(kw))
|
||||||
|
awaited = lambda response, *args: _DirectResponse.setup(promise, response)
|
||||||
|
promise = js.fetch(url, options).then(awaited)
|
||||||
|
_DirectResponse(promise)
|
||||||
|
return promise
|
||||||
18
pyscript.core/src/stdlib/pyscript/ffi.py
Normal file
18
pyscript.core/src/stdlib/pyscript/ffi.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
try:
|
||||||
|
import js
|
||||||
|
from pyodide.ffi import create_proxy as _cp
|
||||||
|
from pyodide.ffi import to_js as _py_tjs
|
||||||
|
|
||||||
|
from_entries = js.Object.fromEntries
|
||||||
|
|
||||||
|
def _tjs(value, **kw):
|
||||||
|
if not hasattr(kw, "dict_converter"):
|
||||||
|
kw["dict_converter"] = from_entries
|
||||||
|
return _py_tjs(value, **kw)
|
||||||
|
|
||||||
|
except:
|
||||||
|
from jsffi import create_proxy as _cp
|
||||||
|
from jsffi import to_js as _tjs
|
||||||
|
|
||||||
|
create_proxy = _cp
|
||||||
|
to_js = _tjs
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import js as globalThis
|
import js as globalThis
|
||||||
|
from polyscript import config as _config
|
||||||
from polyscript import js_modules
|
from polyscript import js_modules
|
||||||
from pyscript.util import NotSupported
|
from pyscript.util import NotSupported
|
||||||
|
|
||||||
RUNNING_IN_WORKER = not hasattr(globalThis, "document")
|
RUNNING_IN_WORKER = not hasattr(globalThis, "document")
|
||||||
|
|
||||||
|
config = json.loads(globalThis.JSON.stringify(_config))
|
||||||
|
|
||||||
|
|
||||||
# allow `from pyscript.js_modules.xxx import yyy`
|
# allow `from pyscript.js_modules.xxx import yyy`
|
||||||
class JSModule:
|
class JSModule:
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
import js
|
||||||
|
|
||||||
|
|
||||||
|
def as_bytearray(buffer):
|
||||||
|
ui8a = js.Uint8Array.new(buffer)
|
||||||
|
size = ui8a.length
|
||||||
|
ba = bytearray(size)
|
||||||
|
for i in range(0, size):
|
||||||
|
ba[i] = ui8a[i]
|
||||||
|
return ba
|
||||||
|
|
||||||
|
|
||||||
class NotSupported:
|
class NotSupported:
|
||||||
"""
|
"""
|
||||||
Small helper that raises exceptions if you try to get/set any attribute on
|
Small helper that raises exceptions if you try to get/set any attribute on
|
||||||
|
|||||||
67
pyscript.core/src/stdlib/pyscript/websocket.py
Normal file
67
pyscript.core/src/stdlib/pyscript/websocket.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import js
|
||||||
|
from pyscript.util import as_bytearray
|
||||||
|
|
||||||
|
code = "code"
|
||||||
|
protocols = "protocols"
|
||||||
|
reason = "reason"
|
||||||
|
|
||||||
|
|
||||||
|
class EventMessage:
|
||||||
|
def __init__(self, event):
|
||||||
|
self._event = event
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
value = getattr(self._event, attr)
|
||||||
|
|
||||||
|
if attr == "data" and not isinstance(value, str):
|
||||||
|
if hasattr(value, "to_py"):
|
||||||
|
return value.to_py()
|
||||||
|
# shims in MicroPython
|
||||||
|
return memoryview(as_bytearray(value))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocket(object):
|
||||||
|
CONNECTING = 0
|
||||||
|
OPEN = 1
|
||||||
|
CLOSING = 2
|
||||||
|
CLOSED = 3
|
||||||
|
|
||||||
|
def __init__(self, **kw):
|
||||||
|
url = kw["url"]
|
||||||
|
if protocols in kw:
|
||||||
|
socket = js.WebSocket.new(url, kw[protocols])
|
||||||
|
else:
|
||||||
|
socket = js.WebSocket.new(url)
|
||||||
|
object.__setattr__(self, "_ws", socket)
|
||||||
|
|
||||||
|
for t in ["onclose", "onerror", "onmessage", "onopen"]:
|
||||||
|
if t in kw:
|
||||||
|
socket[t] = kw[t]
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self._ws, attr)
|
||||||
|
|
||||||
|
def __setattr__(self, attr, value):
|
||||||
|
if attr == "onmessage":
|
||||||
|
self._ws[attr] = lambda e: value(EventMessage(e))
|
||||||
|
else:
|
||||||
|
self._ws[attr] = value
|
||||||
|
|
||||||
|
def close(self, **kw):
|
||||||
|
if code in kw and reason in kw:
|
||||||
|
self._ws.close(kw[code], kw[reason])
|
||||||
|
elif code in kw:
|
||||||
|
self._ws.close(kw[code])
|
||||||
|
else:
|
||||||
|
self._ws.close()
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
if isinstance(data, str):
|
||||||
|
self._ws.send(data)
|
||||||
|
else:
|
||||||
|
buffer = js.Uint8Array.new(len(data))
|
||||||
|
for pos, b in enumerate(data):
|
||||||
|
buffer[pos] = b
|
||||||
|
self._ws.send(buffer)
|
||||||
4
pyscript.core/test-results/.last-run.json
Normal file
4
pyscript.core/test-results/.last-run.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"status": "passed",
|
||||||
|
"failedTests": []
|
||||||
|
}
|
||||||
@@ -8,9 +8,17 @@
|
|||||||
<script type="module" src="../dist/core.js"></script>
|
<script type="module" src="../dist/core.js"></script>
|
||||||
<mpy-config src="config-url/config.json"></mpy-config>
|
<mpy-config src="config-url/config.json"></mpy-config>
|
||||||
<script type="mpy">
|
<script type="mpy">
|
||||||
|
from pyscript import config
|
||||||
|
if config["files"]["{TO}"] != "./runtime":
|
||||||
|
raise Exception("wrong config tree")
|
||||||
|
|
||||||
from runtime import test
|
from runtime import test
|
||||||
</script>
|
</script>
|
||||||
<script type="mpy" worker>
|
<script type="mpy" worker>
|
||||||
|
from pyscript import config
|
||||||
|
if config["files"]["{TO}"] != "./runtime":
|
||||||
|
raise Exception("wrong config tree")
|
||||||
|
|
||||||
from runtime import test
|
from runtime import test
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
95
pyscript.core/test/fetch.html
Normal file
95
pyscript.core/test/fetch.html
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module">
|
||||||
|
import fetch from 'https://esm.run/@webreflection/fetch';
|
||||||
|
|
||||||
|
globalThis.fetch_text = await fetch("config.json").text();
|
||||||
|
globalThis.fetch_json = JSON.stringify(await fetch("config.json").json());
|
||||||
|
globalThis.fetch_buffer = new Uint8Array((await fetch("config.json").arrayBuffer())).length;
|
||||||
|
|
||||||
|
document.head.appendChild(
|
||||||
|
Object.assign(
|
||||||
|
document.createElement('script'),
|
||||||
|
{
|
||||||
|
type: 'module',
|
||||||
|
src: '../dist/core.js'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<script type="mpy" async>
|
||||||
|
import js, json
|
||||||
|
from pyscript import document, fetch
|
||||||
|
|
||||||
|
fetch_text = await (await fetch("config.json")).text()
|
||||||
|
if (fetch_text != js.fetch_text):
|
||||||
|
raise Exception("fetch_text")
|
||||||
|
|
||||||
|
fetch_text = await fetch("config.json").text()
|
||||||
|
if (fetch_text != js.fetch_text):
|
||||||
|
raise Exception("fetch_text")
|
||||||
|
|
||||||
|
fetch_json = await (await fetch("config.json")).json()
|
||||||
|
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||||
|
raise Exception("fetch_json")
|
||||||
|
|
||||||
|
fetch_json = await fetch("config.json").json()
|
||||||
|
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||||
|
raise Exception("fetch_json")
|
||||||
|
|
||||||
|
fetch_buffer = await (await fetch("config.json")).arrayBuffer()
|
||||||
|
if (len(fetch_buffer) != js.fetch_buffer):
|
||||||
|
raise Exception("fetch_buffer")
|
||||||
|
|
||||||
|
fetch_buffer = await fetch("config.json").arrayBuffer()
|
||||||
|
if (len(fetch_buffer) != js.fetch_buffer):
|
||||||
|
raise Exception("fetch_buffer")
|
||||||
|
|
||||||
|
print(await (await fetch("config.json")).bytearray())
|
||||||
|
print(await (await fetch("config.json")).blob())
|
||||||
|
|
||||||
|
if (await fetch("shenanigans.nope")).ok == False:
|
||||||
|
document.documentElement.classList.add('mpy')
|
||||||
|
</script>
|
||||||
|
<script type="py" async>
|
||||||
|
import js, json
|
||||||
|
from pyscript import document, fetch
|
||||||
|
|
||||||
|
fetch_text = await (await fetch("config.json")).text()
|
||||||
|
if (fetch_text != js.fetch_text):
|
||||||
|
raise Exception("fetch_text")
|
||||||
|
|
||||||
|
fetch_text = await fetch("config.json").text()
|
||||||
|
if (fetch_text != js.fetch_text):
|
||||||
|
raise Exception("fetch_text")
|
||||||
|
|
||||||
|
fetch_json = await (await fetch("config.json")).json()
|
||||||
|
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||||
|
raise Exception("fetch_json")
|
||||||
|
|
||||||
|
fetch_json = await fetch("config.json").json()
|
||||||
|
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
|
||||||
|
raise Exception("fetch_json")
|
||||||
|
|
||||||
|
fetch_buffer = await (await fetch("config.json")).arrayBuffer()
|
||||||
|
if (len(fetch_buffer) != js.fetch_buffer):
|
||||||
|
raise Exception("fetch_buffer")
|
||||||
|
|
||||||
|
fetch_buffer = await fetch("config.json").arrayBuffer()
|
||||||
|
if (len(fetch_buffer) != js.fetch_buffer):
|
||||||
|
raise Exception("fetch_buffer")
|
||||||
|
|
||||||
|
print(await (await fetch("config.json")).bytearray())
|
||||||
|
print(await (await fetch("config.json")).blob())
|
||||||
|
|
||||||
|
if (await fetch("shenanigans.nope")).ok == False:
|
||||||
|
document.documentElement.classList.add('py')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
pyscript.core/test/ffi.html
Normal file
26
pyscript.core/test/ffi.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>PyScript FFI</title>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="mpy">
|
||||||
|
from pyscript import document
|
||||||
|
from pyscript.ffi import to_js
|
||||||
|
document.documentElement.classList.add(
|
||||||
|
to_js({"ok": "mpy"}).ok
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
<script type="py">
|
||||||
|
from pyscript import document
|
||||||
|
from pyscript.ffi import to_js
|
||||||
|
document.documentElement.classList.add(
|
||||||
|
to_js({"ok": "py"}).ok
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -78,3 +78,13 @@ test('Pyodide + multiple terminals via Worker', async ({ page }) => {
|
|||||||
await page.goto('http://localhost:8080/test/py-terminals.html');
|
await page.goto('http://localhost:8080/test/py-terminals.html');
|
||||||
await page.waitForSelector('html.first.second');
|
await page.waitForSelector('html.first.second');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('MicroPython + Pyodide fetch', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080/test/fetch.html');
|
||||||
|
await page.waitForSelector('html.mpy.py');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MicroPython + Pyodide ffi', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080/test/ffi.html');
|
||||||
|
await page.waitForSelector('html.mpy.py');
|
||||||
|
});
|
||||||
|
|||||||
2
pyscript.core/test/py-editor/config.toml
Normal file
2
pyscript.core/test/py-editor/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[js_modules.worker]
|
||||||
|
"https://cdn.jsdelivr.net/npm/html-escaper/+esm" = "html_escaper"
|
||||||
55
pyscript.core/test/py-editor/index.html
Normal file
55
pyscript.core/test/py-editor/index.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module">
|
||||||
|
import '../../dist/core.js';
|
||||||
|
|
||||||
|
addEventListener('mpy-editor', async ({ target }) => {
|
||||||
|
if (target.hasAttribute('setup')) {
|
||||||
|
await target.process([
|
||||||
|
'from pyscript import document',
|
||||||
|
// adds class="a-1" to the <html> element
|
||||||
|
'document.documentElement.classList.add(f"a-{a}")',
|
||||||
|
'from js import console',
|
||||||
|
'console.log("Hello JS")',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- a setup node with a config for an env -->
|
||||||
|
<script type="mpy-editor" src="task1.py" config="./config.toml" env="task1" setup></script>
|
||||||
|
<script type="mpy-editor" env="task1">
|
||||||
|
from pyscript.js_modules.html_escaper import escape, unescape
|
||||||
|
print(unescape(escape("<OK>")))
|
||||||
|
a = 1
|
||||||
|
</script>
|
||||||
|
<!-- a share-nothing micropython editor -->
|
||||||
|
<script type="mpy-editor" config="./config.toml">
|
||||||
|
from pyscript.js_modules.html_escaper import escape, unescape
|
||||||
|
print(unescape(escape("<OK>")))
|
||||||
|
b = 2
|
||||||
|
try:
|
||||||
|
print(a)
|
||||||
|
except:
|
||||||
|
print("all good")
|
||||||
|
</script>
|
||||||
|
<!-- a config once micropython env -->
|
||||||
|
<script type="mpy-editor" env="task2" config="./config.toml">
|
||||||
|
from pyscript.js_modules.html_escaper import escape, unescape
|
||||||
|
print(unescape(escape("<OK>")))
|
||||||
|
c = 3
|
||||||
|
try:
|
||||||
|
print(b)
|
||||||
|
except:
|
||||||
|
print("all good")
|
||||||
|
</script>
|
||||||
|
<script type="mpy-editor" env="task2">
|
||||||
|
print(c)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
pyscript.core/test/py-editor/issue-2056.html
Normal file
17
pyscript.core/test/py-editor/issue-2056.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py-editor">
|
||||||
|
print("Hello!")
|
||||||
|
</script>
|
||||||
|
<script type="mpy-editor">
|
||||||
|
print("Hello!")
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
pyscript.core/test/py-editor/task1.py
Normal file
5
pyscript.core/test/py-editor/task1.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from pyscript import window
|
||||||
|
|
||||||
|
window.console.log("OK")
|
||||||
|
|
||||||
|
a = 1
|
||||||
@@ -9,24 +9,10 @@
|
|||||||
<style>.xterm { padding: .5rem; }</style>
|
<style>.xterm { padding: .5rem; }</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="py">
|
<script type="mpy" worker terminal>
|
||||||
def greetings(event):
|
print("µpython")
|
||||||
print('hello world')
|
import code
|
||||||
|
code.interact()
|
||||||
</script>
|
</script>
|
||||||
<py-script worker terminal>
|
|
||||||
# works on both worker and main scripts
|
|
||||||
print("__terminal__", __terminal__)
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pyscript import display, document
|
|
||||||
display("Hello", "PyScript Next - PyTerminal", append=False)
|
|
||||||
print("this should go to the terminal")
|
|
||||||
print("another line")
|
|
||||||
|
|
||||||
# this works as expected
|
|
||||||
print("this goes to stderr", file=sys.stderr)
|
|
||||||
document.addEventListener('click', lambda event: print(event.type));
|
|
||||||
</py-script>
|
|
||||||
<button id="my-button" py-click="greetings">Click me</button>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
6
pyscript.core/test/ws.spec.js
Normal file
6
pyscript.core/test/ws.spec.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('MicroPython WebSocket', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:5037/');
|
||||||
|
await page.waitForSelector('html.ok');
|
||||||
|
});
|
||||||
33
pyscript.core/test/ws/index.html
Normal file
33
pyscript.core/test/ws/index.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="mpy" worker>
|
||||||
|
from pyscript import WebSocket, document
|
||||||
|
|
||||||
|
def onopen(event):
|
||||||
|
print(event.type)
|
||||||
|
ws.send("hello")
|
||||||
|
|
||||||
|
def onmessage(event):
|
||||||
|
print(event.type, event.data)
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
def onclose(event):
|
||||||
|
print(event.type)
|
||||||
|
document.documentElement.classList.add("ok")
|
||||||
|
|
||||||
|
ws = WebSocket(
|
||||||
|
url="ws://localhost:5037/",
|
||||||
|
onopen=onopen,
|
||||||
|
onmessage=onmessage,
|
||||||
|
onclose=onclose
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
pyscript.core/test/ws/index.js
Normal file
33
pyscript.core/test/ws/index.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { serve, file } from 'bun';
|
||||||
|
|
||||||
|
import path, { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const dir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
serve({
|
||||||
|
port: 5037,
|
||||||
|
fetch(req, server) {
|
||||||
|
if (server.upgrade(req)) return;
|
||||||
|
const url = new URL(req.url);
|
||||||
|
let { pathname } = url;
|
||||||
|
if (pathname === '/') pathname = '/index.html';
|
||||||
|
else if (/^\/dist\//.test(pathname)) pathname = `/../..${pathname}`;
|
||||||
|
else if (pathname === '/favicon.ico')
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
const response = new Response(file(`${dir}${pathname}`));
|
||||||
|
const { headers } = response;
|
||||||
|
headers.set('Cross-Origin-Opener-Policy', 'same-origin');
|
||||||
|
headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||||
|
headers.set('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
websocket: {
|
||||||
|
message(ws, message) {
|
||||||
|
ws.send(message);
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
4
pyscript.core/types/3rd-party/xterm_addon-web-links.d.ts
vendored
Normal file
4
pyscript.core/types/3rd-party/xterm_addon-web-links.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare var r: any;
|
||||||
|
declare var n: any;
|
||||||
|
declare var t: {};
|
||||||
|
export { r as WebLinksAddon, n as __esModule, t as default };
|
||||||
12
pyscript.core/types/core.d.ts
vendored
12
pyscript.core/types/core.d.ts
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
export function offline_interpreter(config: any): string;
|
||||||
|
import { stdlib } from "./stdlib.js";
|
||||||
|
import { optional } from "./stdlib.js";
|
||||||
import TYPES from "./types.js";
|
import TYPES from "./types.js";
|
||||||
/**
|
/**
|
||||||
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||||
@@ -38,11 +41,7 @@ declare const exportedHooks: {
|
|||||||
};
|
};
|
||||||
worker: {
|
worker: {
|
||||||
onReady: Set<Function>;
|
onReady: Set<Function>;
|
||||||
onBeforeRun: Set<Function>; /**
|
onBeforeRun: Set<Function>;
|
||||||
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
|
|
||||||
* It either throws an error if the 'src' can't be fetched or it returns a fallback
|
|
||||||
* content as source.
|
|
||||||
*/
|
|
||||||
onBeforeRunAsync: Set<Function>;
|
onBeforeRunAsync: Set<Function>;
|
||||||
onAfterRun: Set<Function>;
|
onAfterRun: Set<Function>;
|
||||||
onAfterRunAsync: Set<Function>;
|
onAfterRunAsync: Set<Function>;
|
||||||
@@ -54,5 +53,4 @@ declare const exportedHooks: {
|
|||||||
};
|
};
|
||||||
declare const exportedConfig: {};
|
declare const exportedConfig: {};
|
||||||
declare const exportedWhenDefined: (type: string) => Promise<any>;
|
declare const exportedWhenDefined: (type: string) => Promise<any>;
|
||||||
import sync from "./sync.js";
|
export { stdlib, optional, TYPES, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
||||||
export { TYPES, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
|
||||||
|
|||||||
2
pyscript.core/types/hooks.d.ts
vendored
2
pyscript.core/types/hooks.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
export function main(name: any): any;
|
export function main(name: any): any;
|
||||||
export function worker(name: any): any;
|
export function worker(name: any): any;
|
||||||
export function codeFor(branch: any): {};
|
export function codeFor(branch: any, type: any): {};
|
||||||
export function createFunction(self: any, name: any): any;
|
export function createFunction(self: any, name: any): any;
|
||||||
export namespace hooks {
|
export namespace hooks {
|
||||||
namespace main {
|
namespace main {
|
||||||
|
|||||||
3
pyscript.core/types/plugins/py-terminal.d.ts
vendored
3
pyscript.core/types/plugins/py-terminal.d.ts
vendored
@@ -1,2 +1 @@
|
|||||||
declare const _default: Promise<void>;
|
export {};
|
||||||
export default _default;
|
|
||||||
|
|||||||
4
pyscript.core/types/stdlib.d.ts
vendored
4
pyscript.core/types/stdlib.d.ts
vendored
@@ -1,2 +1,2 @@
|
|||||||
declare const _default: string;
|
export const stdlib: string;
|
||||||
export default _default;
|
export const optional: string;
|
||||||
|
|||||||
3
pyscript.core/types/stdlib/pyscript.d.ts
vendored
3
pyscript.core/types/stdlib/pyscript.d.ts
vendored
@@ -3,8 +3,11 @@ declare namespace _default {
|
|||||||
"__init__.py": string;
|
"__init__.py": string;
|
||||||
"display.py": string;
|
"display.py": string;
|
||||||
"event_handling.py": string;
|
"event_handling.py": string;
|
||||||
|
"fetch.py": string;
|
||||||
|
"ffi.py": string;
|
||||||
"magic_js.py": string;
|
"magic_js.py": string;
|
||||||
"util.py": string;
|
"util.py": string;
|
||||||
|
"websocket.py": string;
|
||||||
};
|
};
|
||||||
let pyweb: {
|
let pyweb: {
|
||||||
"__init__.py": string;
|
"__init__.py": string;
|
||||||
|
|||||||
Reference in New Issue
Block a user