mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
4
.github/workflows/publish-release.yml
vendored
4
.github/workflows/publish-release.yml
vendored
@@ -46,6 +46,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:
|
||||
|
||||
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.4.2
|
||||
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,9 +1,11 @@
|
||||
.eslintrc.cjs
|
||||
eslint.config.mjs
|
||||
.pytest_cache/
|
||||
node_modules/
|
||||
rollup/
|
||||
test/
|
||||
tests/
|
||||
test-results/
|
||||
src/stdlib/_pyscript
|
||||
src/stdlib/pyscript.py
|
||||
package-lock.json
|
||||
|
||||
22
pyscript.core/eslint.config.mjs
Normal file
22
pyscript.core/eslint.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import globals from "globals";
|
||||
import js from "@eslint/js";
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
{
|
||||
ignores: ["**/3rd-party/"],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.es2021,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-implicit-globals": ["error"],
|
||||
},
|
||||
},
|
||||
];
|
||||
1684
pyscript.core/package-lock.json
generated
1684
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.1",
|
||||
"type": "module",
|
||||
"description": "PyScript",
|
||||
"module": "./index.js",
|
||||
@@ -20,13 +20,15 @@
|
||||
},
|
||||
"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",
|
||||
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run ts && npm run test:mpy; 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",
|
||||
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/ || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/mpy.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||
"test:ws": "bun test/ws/index.js & playwright test test/ws.spec.js",
|
||||
"dev": "node dev.cjs",
|
||||
"release": "npm run build && npm run zip",
|
||||
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
|
||||
@@ -42,32 +44,34 @@
|
||||
"dependencies": {
|
||||
"@ungap/with-resolvers": "^0.1.0",
|
||||
"basic-devtools": "^0.1.6",
|
||||
"polyscript": "^0.12.6",
|
||||
"polyscript": "^0.14.4",
|
||||
"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.0",
|
||||
"@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.29.1",
|
||||
"@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",
|
||||
"bun": "^1.1.21",
|
||||
"chokidar": "^3.6.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"eslint": "^9.1.1",
|
||||
"rollup": "^4.16.4",
|
||||
"eslint": "^9.8.0",
|
||||
"flatted": "^3.3.1",
|
||||
"rollup": "^4.19.1",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"static-handler": "^0.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript": "^5.5.4",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-readline": "^1.1.1"
|
||||
},
|
||||
|
||||
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)));
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -19,15 +19,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 +84,7 @@ const [
|
||||
|
||||
export {
|
||||
TYPES,
|
||||
relative_url,
|
||||
exportedPyWorker as PyWorker,
|
||||
exportedMPWorker as MPWorker,
|
||||
exportedHooks as hooks,
|
||||
@@ -84,6 +92,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 +158,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
|
||||
@@ -294,7 +306,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);
|
||||
},
|
||||
|
||||
@@ -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,6 @@
|
||||
// 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";
|
||||
|
||||
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>`;
|
||||
|
||||
@@ -34,14 +34,28 @@ 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()));
|
||||
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.text()),
|
||||
]);
|
||||
details.config = parse(toml);
|
||||
} else if (config.endsWith(".json")) {
|
||||
details.config = await fetch(config).then((r) => r.json());
|
||||
} else {
|
||||
details.configURL = relative_url("./config.txt");
|
||||
details.config = JSON.parse(config);
|
||||
}
|
||||
details.version = offline_interpreter(details.config);
|
||||
} else {
|
||||
details.config = {};
|
||||
}
|
||||
|
||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||
@@ -57,7 +71,7 @@ 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'>${
|
||||
@@ -83,21 +97,24 @@ async function execute({ currentTarget }) {
|
||||
});
|
||||
}
|
||||
|
||||
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 +134,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 +152,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 +164,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 +187,16 @@ const init = async (script, type, interpreter) => {
|
||||
|
||||
configs.set(env, hasConfig);
|
||||
|
||||
const source = script.src
|
||||
let source = script.src
|
||||
? await fetch(script.src).then((b) => b.text())
|
||||
: script.textContent;
|
||||
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 +205,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 notify = () => {
|
||||
const event = new Event(`${type}-editor`, { bubbles: true });
|
||||
script.dispatchEvent(event);
|
||||
};
|
||||
|
||||
if (isSetup) {
|
||||
execute.call(context, { currentTarget: null });
|
||||
await context.handleEvent({ currentTarget: null });
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
|
||||
const selector = script.getAttribute("target");
|
||||
|
||||
let target;
|
||||
if (selector) {
|
||||
target =
|
||||
document.getElementById(selector) ||
|
||||
@@ -202,8 +297,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 +310,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 +322,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();
|
||||
notify();
|
||||
};
|
||||
|
||||
// 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,22 +19,23 @@ def when(event_type=None, selector=None):
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
|
||||
from pyscript.web import Element, ElementCollection
|
||||
|
||||
if isinstance(selector, str):
|
||||
elements = document.querySelectorAll(selector)
|
||||
# TODO: This is a hack that will be removed when pyscript becomes a package
|
||||
# and we can better manage the imports without circular dependencies
|
||||
elif isinstance(selector, Element):
|
||||
elements = [selector._dom_element]
|
||||
elif isinstance(selector, ElementCollection):
|
||||
elements = [el._dom_element for el in 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]
|
||||
if isinstance(selector, list):
|
||||
elements = selector
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid selector: {selector}. Selector must"
|
||||
" be a string, a pydom.Element or a pydom.ElementCollection."
|
||||
)
|
||||
elements = [selector]
|
||||
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
# Function doesn't receive events
|
||||
@@ -47,13 +48,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
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)
|
||||
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()
|
||||
@@ -8,9 +8,17 @@
|
||||
<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>
|
||||
|
||||
4
pyscript.core/test/issue-7015/config.toml
Normal file
4
pyscript.core/test/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/test/issue-7015/index.html
Normal file
17
pyscript.core/test/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/test/issue-7015/main.py
Normal file
12
pyscript.core/test/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")
|
||||
@@ -88,3 +88,13 @@ test('MicroPython + Pyodide ffi', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/test/ffi.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
});
|
||||
|
||||
test('MicroPython + Storage', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/test/storage.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
test('MicroPython + workers', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/test/workers/index.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
});
|
||||
|
||||
@@ -4,7 +4,21 @@
|
||||
<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>
|
||||
<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 -->
|
||||
@@ -15,7 +29,7 @@
|
||||
a = 1
|
||||
</script>
|
||||
<!-- a share-nothing micropython editor -->
|
||||
<script type="mpy-editor" config="./config.toml">
|
||||
<script type="mpy-editor" config='{"js_modules":{"worker":{"https://cdn.jsdelivr.net/npm/html-escaper/+esm":"html_escaper"}}}'>
|
||||
from pyscript.js_modules.html_escaper import escape, unescape
|
||||
print(unescape(escape("<OK>")))
|
||||
b = 2
|
||||
|
||||
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>
|
||||
16
pyscript.core/test/py-editor/service-worker.html
Normal file
16
pyscript.core/test/py-editor/service-worker.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="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy-editor" service-worker="./sw.js">
|
||||
from pyscript import document
|
||||
|
||||
document.body.append("OK")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
pyscript.core/test/py-editor/sw.js
Normal file
1
pyscript.core/test/py-editor/sw.js
Normal file
@@ -0,0 +1 @@
|
||||
const{isArray:e}=Array,t=new Map,s=e=>{e.stopImmediatePropagation(),e.preventDefault()};var n=Object.freeze({__proto__:null,activate:e=>e.waitUntil(clients.claim()),fetch:e=>{const{request:n}=e;"POST"===n.method&&n.url===`${location.href}?sabayon`&&(s(e),e.respondWith(n.json().then((async e=>{const{promise:s,resolve:o}=Promise.withResolvers(),a=e.join(",");t.set(a,o);for(const t of await clients.matchAll())t.postMessage(e);return s.then((e=>new Response(`[${e.join(",")}]`,n.headers)))}))))},install:()=>skipWaiting(),message:n=>{const{data:o}=n;if(e(o)&&4===o.length){const[e,a,i,r]=o,l=[e,a,i].join(",");t.has(l)&&(s(n),t.get(l)(r),t.delete(l))}}});for(const e in n)addEventListener(e,n[e]);
|
||||
@@ -10,6 +10,6 @@
|
||||
</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>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<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")
|
||||
|
||||
|
||||
18
pyscript.core/test/py-terminals/index.html
Normal file
18
pyscript.core/test/py-terminals/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>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="./no-repl.html">Prompt: NO REPL</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./repl.html">Prompt: REPL</a>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
20
pyscript.core/test/py-terminals/no-repl.html
Normal file
20
pyscript.core/test/py-terminals/no-repl.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PyTerminal Prompt: NO REPL</title>
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<style>.xterm { padding: .5rem; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" worker terminal>
|
||||
prompt = input("Say something: ")
|
||||
print("You said, ", prompt)
|
||||
</script>
|
||||
<script type="py" worker terminal>
|
||||
prompt = input("Say something: ")
|
||||
print("You said, ", prompt)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
28
pyscript.core/test/py-terminals/repl.html
Normal file
28
pyscript.core/test/py-terminals/repl.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" />
|
||||
<title>PyTerminal Prompt: REPL</title>
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<style>.xterm { padding: .5rem; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" worker terminal>
|
||||
import code
|
||||
code.interact()
|
||||
|
||||
prompt = input("Say something: ")
|
||||
print("You said, ", prompt)
|
||||
</script>
|
||||
<script type="py" worker terminal>
|
||||
import code
|
||||
code.interact()
|
||||
|
||||
# Pyodide won't execute this ... ever
|
||||
# this should be tested manually
|
||||
prompt = input("Say something: ")
|
||||
print("You said, ", prompt)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
27
pyscript.core/test/py_modules.html
Normal file
27
pyscript.core/test/py_modules.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" async>
|
||||
from pyscript import py_import, js_import, window
|
||||
|
||||
window.console.time("first")
|
||||
matplotlib, regex, = await py_import("matplotlib", "regex")
|
||||
window.console.timeEnd("first")
|
||||
|
||||
window.console.time("second")
|
||||
matplotlib, regex, = await py_import("matplotlib", "regex")
|
||||
window.console.timeEnd("second")
|
||||
|
||||
print(matplotlib, regex)
|
||||
|
||||
escaper, = await js_import("https://esm.run/html-escaper")
|
||||
window.console.log(escaper)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,7 +8,9 @@
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" src="pydom.py"></script>
|
||||
<script type="mpy" src="pydom.py"></script>
|
||||
|
||||
<div id="system-info"></div>
|
||||
|
||||
<button id="just-a-button">Click For Time</button>
|
||||
<button id="color-button">Click For Color</button>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime as dt
|
||||
|
||||
from pyscript import display, when
|
||||
from pyweb import pydom
|
||||
from pyscript.web import dom
|
||||
|
||||
display(sys.version, target="system-info")
|
||||
|
||||
|
||||
@when("click", "#just-a-button")
|
||||
@@ -16,18 +19,15 @@ def on_click():
|
||||
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")
|
||||
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 = dom["#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)
|
||||
dom["#result"].style["background-color"] = "white"
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
<body>
|
||||
<script type="mpy" src="pydom.py"></script>
|
||||
|
||||
<div id="system-info"></div>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" src="./run_tests.py" config="./tests.toml"></script>
|
||||
<script type="py" src="/test/pyscript_dom/run_tests.py" config="/test/pyscript_dom/tests.toml"></script>
|
||||
|
||||
<h1>pyscript.dom Tests</h1>
|
||||
<p>You can pass test parameters to this test suite by passing them as query params on the url.
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from pyscript import document, when
|
||||
from pyweb import pydom
|
||||
from pyscript.web import Element, ElementCollection, div, p, page
|
||||
|
||||
|
||||
class TestDocument:
|
||||
def test__element(self):
|
||||
assert pydom._js == document
|
||||
|
||||
def test_no_parent(self):
|
||||
assert pydom.parent is None
|
||||
|
||||
def test_create_element(self):
|
||||
new_el = pydom.create("div")
|
||||
assert isinstance(new_el, pydom.BaseElement)
|
||||
assert new_el._js.tagName == "DIV"
|
||||
# EXPECT the new element to be associated with the document
|
||||
assert new_el.parent == None
|
||||
assert page.body._dom_element == document.body
|
||||
assert page.head._dom_element == document.head
|
||||
|
||||
|
||||
def test_getitem_by_id():
|
||||
@@ -26,14 +14,14 @@ def test_getitem_by_id():
|
||||
txt = "You found test_id_selector"
|
||||
selector = f"#{id_}"
|
||||
# EXPECT the element to be found by id
|
||||
result = pydom[selector]
|
||||
result = page.find(selector)
|
||||
div = result[0]
|
||||
# EXPECT the element text value to match what we expect and what
|
||||
# the JS document.querySelector API would return
|
||||
assert document.querySelector(selector).innerHTML == div.html == txt
|
||||
assert document.querySelector(selector).innerHTML == div.innerHTML == txt
|
||||
# EXPECT the results to be of the right types
|
||||
assert isinstance(div, pydom.BaseElement)
|
||||
assert isinstance(result, pydom.ElementCollection)
|
||||
assert isinstance(div, Element)
|
||||
assert isinstance(result, ElementCollection)
|
||||
|
||||
|
||||
def test_getitem_by_class():
|
||||
@@ -43,8 +31,7 @@ def test_getitem_by_class():
|
||||
"test_selector_w_children_child_1",
|
||||
]
|
||||
expected_class = "a-test-class"
|
||||
result = pydom[f".{expected_class}"]
|
||||
div = result[0]
|
||||
result = page.find(f".{expected_class}")
|
||||
|
||||
# EXPECT to find exact number of elements with the class in the page (== 3)
|
||||
assert len(result) == 3
|
||||
@@ -54,40 +41,40 @@ def test_getitem_by_class():
|
||||
|
||||
|
||||
def test_read_n_write_collection_elements():
|
||||
elements = pydom[".multi-elems"]
|
||||
elements = page.find(".multi-elems")
|
||||
|
||||
for element in elements:
|
||||
assert element.html == f"Content {element.id.replace('#', '')}"
|
||||
assert element.innerHTML == f"Content {element.id.replace('#', '')}"
|
||||
|
||||
new_content = "New Content"
|
||||
elements.html = new_content
|
||||
elements.innerHTML = new_content
|
||||
for element in elements:
|
||||
assert element.html == new_content
|
||||
assert element.innerHTML == new_content
|
||||
|
||||
|
||||
class TestElement:
|
||||
def test_query(self):
|
||||
# GIVEN an existing element on the page, with at least 1 child element
|
||||
id_ = "test_selector_w_children"
|
||||
parent_div = pydom[f"#{id_}"][0]
|
||||
parent_div = page.find(f"#{id_}")[0]
|
||||
|
||||
# EXPECT it to be able to query for the first child element
|
||||
div = parent_div.find("div")[0]
|
||||
|
||||
# EXPECT the new element to be associated with the parent
|
||||
assert div.parent == parent_div
|
||||
# EXPECT the new element to be a BaseElement
|
||||
assert isinstance(div, pydom.BaseElement)
|
||||
# EXPECT the new element to be an Element
|
||||
assert isinstance(div, Element)
|
||||
# EXPECT the div attributes to be == to how they are configured in the page
|
||||
assert div.html == "Child 1"
|
||||
assert div.innerHTML == "Child 1"
|
||||
assert div.id == "test_selector_w_children_child_1"
|
||||
|
||||
def test_equality(self):
|
||||
# GIVEN 2 different Elements pointing to the same underlying element
|
||||
id_ = "test_id_selector"
|
||||
selector = f"#{id_}"
|
||||
div = pydom[selector][0]
|
||||
div2 = pydom[selector][0]
|
||||
div = page.find(selector)[0]
|
||||
div2 = page.find(selector)[0]
|
||||
|
||||
# EXPECT them to be equal
|
||||
assert div == div2
|
||||
@@ -95,34 +82,34 @@ class TestElement:
|
||||
assert div is not div2
|
||||
|
||||
# EXPECT their value to always be equal
|
||||
assert div.html == div2.html
|
||||
div.html = "some value"
|
||||
assert div.innerHTML == div2.innerHTML
|
||||
div.innerHTML = "some value"
|
||||
|
||||
assert div.html == div2.html == "some value"
|
||||
assert div.innerHTML == div2.innerHTML == "some value"
|
||||
|
||||
def test_append_element(self):
|
||||
id_ = "element-append-tests"
|
||||
div = pydom[f"#{id_}"][0]
|
||||
div = page.find(f"#{id_}")[0]
|
||||
len_children_before = len(div.children)
|
||||
new_el = div.create("p")
|
||||
new_el = p("new element")
|
||||
div.append(new_el)
|
||||
assert len(div.children) == len_children_before + 1
|
||||
assert div.children[-1] == new_el
|
||||
|
||||
def test_append_js_element(self):
|
||||
def test_append_dom_element_element(self):
|
||||
id_ = "element-append-tests"
|
||||
div = pydom[f"#{id_}"][0]
|
||||
div = page.find(f"#{id_}")[0]
|
||||
len_children_before = len(div.children)
|
||||
new_el = div.create("p")
|
||||
div.append(new_el._js)
|
||||
new_el = p("new element")
|
||||
div.append(new_el._dom_element)
|
||||
assert len(div.children) == len_children_before + 1
|
||||
assert div.children[-1] == new_el
|
||||
|
||||
def test_append_collection(self):
|
||||
id_ = "element-append-tests"
|
||||
div = pydom[f"#{id_}"][0]
|
||||
div = page.find(f"#{id_}")[0]
|
||||
len_children_before = len(div.children)
|
||||
collection = pydom[".collection"]
|
||||
collection = page.find(".collection")
|
||||
div.append(collection)
|
||||
assert len(div.children) == len_children_before + len(collection)
|
||||
|
||||
@@ -132,24 +119,24 @@ class TestElement:
|
||||
def test_read_classes(self):
|
||||
id_ = "test_class_selector"
|
||||
expected_class = "a-test-class"
|
||||
div = pydom[f"#{id_}"][0]
|
||||
div = page.find(f"#{id_}")[0]
|
||||
assert div.classes == [expected_class]
|
||||
|
||||
def test_add_remove_class(self):
|
||||
id_ = "div-no-classes"
|
||||
classname = "tester-class"
|
||||
div = pydom[f"#{id_}"][0]
|
||||
div = page.find(f"#{id_}")[0]
|
||||
assert not div.classes
|
||||
div.add_class(classname)
|
||||
same_div = pydom[f"#{id_}"][0]
|
||||
div.classes.add(classname)
|
||||
same_div = page.find(f"#{id_}")[0]
|
||||
assert div.classes == [classname] == same_div.classes
|
||||
div.remove_class(classname)
|
||||
div.classes.remove(classname)
|
||||
assert div.classes == [] == same_div.classes
|
||||
|
||||
def test_when_decorator(self):
|
||||
called = False
|
||||
|
||||
just_a_button = pydom["#a-test-button"][0]
|
||||
just_a_button = page.find("#a-test-button")[0]
|
||||
|
||||
@when("click", just_a_button)
|
||||
def on_click(event):
|
||||
@@ -157,45 +144,49 @@ class TestElement:
|
||||
called = True
|
||||
|
||||
# Now let's simulate a click on the button (using the low level JS API)
|
||||
# so we don't risk pydom getting in the way
|
||||
# so we don't risk dom getting in the way
|
||||
assert not called
|
||||
just_a_button._js.click()
|
||||
just_a_button._dom_element.click()
|
||||
|
||||
assert called
|
||||
|
||||
def test_html_attribute(self):
|
||||
def test_inner_html_attribute(self):
|
||||
# GIVEN an existing element on the page with a known empty text content
|
||||
div = pydom["#element_attribute_tests"][0]
|
||||
div = page.find("#element_attribute_tests")[0]
|
||||
|
||||
# WHEN we set the html attribute
|
||||
div.html = "<b>New Content</b>"
|
||||
div.innerHTML = "<b>New Content</b>"
|
||||
|
||||
# EXPECT the element html and underlying JS Element innerHTML property
|
||||
# to match what we expect and what
|
||||
assert div.html == div._js.innerHTML == "<b>New Content</b>"
|
||||
assert div.text == div._js.textContent == "New Content"
|
||||
assert div.innerHTML == div._dom_element.innerHTML == "<b>New Content</b>"
|
||||
assert div.textContent == div._dom_element.textContent == "New Content"
|
||||
|
||||
def test_text_attribute(self):
|
||||
# GIVEN an existing element on the page with a known empty text content
|
||||
div = pydom["#element_attribute_tests"][0]
|
||||
div = page.find("#element_attribute_tests")[0]
|
||||
|
||||
# WHEN we set the html attribute
|
||||
div.text = "<b>New Content</b>"
|
||||
div.textContent = "<b>New Content</b>"
|
||||
|
||||
# EXPECT the element html and underlying JS Element innerHTML property
|
||||
# to match what we expect and what
|
||||
assert div.html == div._js.innerHTML == "<b>New Content</b>"
|
||||
assert div.text == div._js.textContent == "<b>New Content</b>"
|
||||
assert (
|
||||
div.innerHTML
|
||||
== div._dom_element.innerHTML
|
||||
== "<b>New Content</b>"
|
||||
)
|
||||
assert div.textContent == div._dom_element.textContent == "<b>New Content</b>"
|
||||
|
||||
|
||||
class TestCollection:
|
||||
def test_iter_eq_children(self):
|
||||
elements = pydom[".multi-elems"]
|
||||
assert [el for el in elements] == [el for el in elements.children]
|
||||
elements = page.find(".multi-elems")
|
||||
assert [el for el in elements] == [el for el in elements.elements]
|
||||
assert len(elements) == 3
|
||||
|
||||
def test_slices(self):
|
||||
elements = pydom[".multi-elems"]
|
||||
elements = page.find(".multi-elems")
|
||||
assert elements[0]
|
||||
_slice = elements[:2]
|
||||
assert len(_slice) == 2
|
||||
@@ -205,26 +196,26 @@ class TestCollection:
|
||||
|
||||
def test_style_rule(self):
|
||||
selector = ".multi-elems"
|
||||
elements = pydom[selector]
|
||||
elements = page.find(selector)
|
||||
for el in elements:
|
||||
assert el.style["background-color"] != "red"
|
||||
|
||||
elements.style["background-color"] = "red"
|
||||
|
||||
for i, el in enumerate(pydom[selector]):
|
||||
for i, el in enumerate(page.find(selector)):
|
||||
assert elements[i].style["background-color"] == "red"
|
||||
assert el.style["background-color"] == "red"
|
||||
|
||||
elements.style.remove("background-color")
|
||||
|
||||
for i, el in enumerate(pydom[selector]):
|
||||
for i, el in enumerate(page.find(selector)):
|
||||
assert el.style["background-color"] != "red"
|
||||
assert elements[i].style["background-color"] != "red"
|
||||
|
||||
def test_when_decorator(self):
|
||||
called = False
|
||||
|
||||
buttons_collection = pydom["button"]
|
||||
buttons_collection = page.find("button")
|
||||
|
||||
@when("click", buttons_collection)
|
||||
def on_click(event):
|
||||
@@ -232,42 +223,43 @@ class TestCollection:
|
||||
called = True
|
||||
|
||||
# Now let's simulate a click on the button (using the low level JS API)
|
||||
# so we don't risk pydom getting in the way
|
||||
# so we don't risk dom getting in the way
|
||||
assert not called
|
||||
for button in buttons_collection:
|
||||
button._js.click()
|
||||
button._dom_element.click()
|
||||
assert called
|
||||
called = False
|
||||
|
||||
|
||||
class TestCreation:
|
||||
def test_create_document_element(self):
|
||||
new_el = pydom.create("div")
|
||||
# TODO: This test should probably be removed since it's testing the elements
|
||||
# module.
|
||||
new_el = div("new element")
|
||||
new_el.id = "new_el_id"
|
||||
assert isinstance(new_el, pydom.BaseElement)
|
||||
assert new_el._js.tagName == "DIV"
|
||||
assert isinstance(new_el, Element)
|
||||
assert new_el._dom_element.tagName == "DIV"
|
||||
# EXPECT the new element to be associated with the document
|
||||
assert new_el.parent == None
|
||||
pydom.body.append(new_el)
|
||||
assert new_el.parent is None
|
||||
page.body.append(new_el)
|
||||
|
||||
assert pydom["#new_el_id"][0].parent == pydom.body
|
||||
assert page.find("#new_el_id")[0].parent == page.body
|
||||
|
||||
def test_create_element_child(self):
|
||||
selector = "#element-creation-test"
|
||||
parent_div = pydom[selector][0]
|
||||
parent_div = page.find(selector)[0]
|
||||
|
||||
# Creating an element from another element automatically creates that element
|
||||
# as a child of the original element
|
||||
new_el = parent_div.create(
|
||||
"p", classes=["code-description"], html="Ciao PyScripters!"
|
||||
)
|
||||
new_el = p("a div", classes=["code-description"], innerHTML="Ciao PyScripters!")
|
||||
parent_div.append(new_el)
|
||||
|
||||
assert isinstance(new_el, Element)
|
||||
assert new_el._dom_element.tagName == "P"
|
||||
|
||||
assert isinstance(new_el, pydom.BaseElement)
|
||||
assert new_el._js.tagName == "P"
|
||||
# EXPECT the new element to be associated with the document
|
||||
assert new_el.parent == parent_div
|
||||
|
||||
assert pydom[selector][0].children[0] == new_el
|
||||
assert page.find(selector)[0].children[0] == new_el
|
||||
|
||||
|
||||
class TestInput:
|
||||
@@ -281,10 +273,10 @@ class TestInput:
|
||||
def test_value(self):
|
||||
for id_ in self.input_ids:
|
||||
expected_type = id_.split("_")[-1]
|
||||
result = pydom[f"#{id_}"]
|
||||
result = page.find(f"#{id_}")
|
||||
input_el = result[0]
|
||||
assert input_el._js.type == expected_type
|
||||
assert input_el.value == f"Content {id_}" == input_el._js.value
|
||||
assert input_el._dom_element.type == expected_type
|
||||
assert input_el.value == f"Content {id_}" == input_el._dom_element.value
|
||||
|
||||
# Check that we can set the value
|
||||
new_value = f"New Value {expected_type}"
|
||||
@@ -299,7 +291,7 @@ class TestInput:
|
||||
|
||||
def test_set_value_collection(self):
|
||||
for id_ in self.input_ids:
|
||||
input_el = pydom[f"#{id_}"]
|
||||
input_el = page.find(f"#{id_}")
|
||||
|
||||
assert input_el.value[0] == f"Content {id_}" == input_el[0].value
|
||||
|
||||
@@ -307,36 +299,35 @@ class TestInput:
|
||||
input_el.value = new_value
|
||||
assert input_el.value[0] == new_value == input_el[0].value
|
||||
|
||||
def test_element_without_value(self):
|
||||
result = pydom[f"#tests-terminal"][0]
|
||||
with pytest.raises(AttributeError):
|
||||
result.value = "some value"
|
||||
|
||||
def test_element_without_collection(self):
|
||||
result = pydom[f"#tests-terminal"]
|
||||
with pytest.raises(AttributeError):
|
||||
result.value = "some value"
|
||||
|
||||
def test_element_without_collection(self):
|
||||
result = pydom[f"#tests-terminal"]
|
||||
with pytest.raises(AttributeError):
|
||||
result.value = "some value"
|
||||
# TODO: We only attach attributes to the classes that have them now which means we
|
||||
# would have to have some other way to help users if using attributes that aren't
|
||||
# actually on the class. Maybe a job for __setattr__?
|
||||
#
|
||||
# def test_element_without_value(self):
|
||||
# result = page.find(f"#tests-terminal"][0]
|
||||
# with pytest.raises(AttributeError):
|
||||
# result.value = "some value"
|
||||
#
|
||||
# def test_element_without_value_via_collection(self):
|
||||
# result = page.find(f"#tests-terminal"]
|
||||
# with pytest.raises(AttributeError):
|
||||
# result.value = "some value"
|
||||
|
||||
|
||||
class TestSelect:
|
||||
def test_select_options_iter(self):
|
||||
select = pydom[f"#test_select_element_w_options"][0]
|
||||
select = page.find(f"#test_select_element_w_options")[0]
|
||||
|
||||
for i, option in enumerate(select.options, 1):
|
||||
assert option.value == f"{i}"
|
||||
assert option.html == f"Option {i}"
|
||||
assert option.innerHTML == f"Option {i}"
|
||||
|
||||
def test_select_options_len(self):
|
||||
select = pydom[f"#test_select_element_w_options"][0]
|
||||
select = page.find(f"#test_select_element_w_options")[0]
|
||||
assert len(select.options) == 2
|
||||
|
||||
def test_select_options_clear(self):
|
||||
select = pydom[f"#test_select_element_to_clear"][0]
|
||||
select = page.find(f"#test_select_element_to_clear")[0]
|
||||
assert len(select.options) == 3
|
||||
|
||||
select.options.clear()
|
||||
@@ -345,7 +336,7 @@ class TestSelect:
|
||||
|
||||
def test_select_element_add(self):
|
||||
# GIVEN the existing select element with no options
|
||||
select = pydom[f"#test_select_element"][0]
|
||||
select = page.find(f"#test_select_element")[0]
|
||||
|
||||
# EXPECT the select element to have no options
|
||||
assert len(select.options) == 0
|
||||
@@ -357,7 +348,7 @@ class TestSelect:
|
||||
# we passed in
|
||||
assert len(select.options) == 1
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].html == "Option 1"
|
||||
assert select.options[0].innerHTML == "Option 1"
|
||||
|
||||
# WHEN we add another option (blank this time)
|
||||
select.options.add("")
|
||||
@@ -367,7 +358,7 @@ class TestSelect:
|
||||
|
||||
# EXPECT the last option to have an empty value and html
|
||||
assert select.options[1].value == ""
|
||||
assert select.options[1].html == ""
|
||||
assert select.options[1].innerHTML == ""
|
||||
|
||||
# WHEN we add another option (this time adding it in between the other 2
|
||||
# options by using an integer index)
|
||||
@@ -378,11 +369,11 @@ class TestSelect:
|
||||
|
||||
# EXPECT the middle option to have the value and html we passed in
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].html == "Option 1"
|
||||
assert select.options[0].innerHTML == "Option 1"
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[1].html == "Option 2"
|
||||
assert select.options[1].innerHTML == "Option 2"
|
||||
assert select.options[2].value == ""
|
||||
assert select.options[2].html == ""
|
||||
assert select.options[2].innerHTML == ""
|
||||
|
||||
# WHEN we add another option (this time adding it in between the other 2
|
||||
# options but using the option itself)
|
||||
@@ -395,38 +386,48 @@ class TestSelect:
|
||||
|
||||
# EXPECT the middle option to have the value and html we passed in
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].html == "Option 1"
|
||||
assert select.options[0].selected == select.options[0]._js.selected == False
|
||||
assert select.options[0].innerHTML == "Option 1"
|
||||
assert (
|
||||
select.options[0].selected
|
||||
== select.options[0]._dom_element.selected
|
||||
== False
|
||||
)
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[1].html == "Option 2"
|
||||
assert select.options[1].innerHTML == "Option 2"
|
||||
assert select.options[2].value == "3"
|
||||
assert select.options[2].html == "Option 3"
|
||||
assert select.options[2].selected == select.options[2]._js.selected == True
|
||||
assert select.options[2].innerHTML == "Option 3"
|
||||
assert (
|
||||
select.options[2].selected
|
||||
== select.options[2]._dom_element.selected
|
||||
== True
|
||||
)
|
||||
assert select.options[3].value == ""
|
||||
assert select.options[3].html == ""
|
||||
assert select.options[3].innerHTML == ""
|
||||
|
||||
# WHEN we add another option (this time adding it in between the other 2
|
||||
# options but using the JS element of the option itself)
|
||||
select.options.add(value="2a", html="Option 2a", before=select.options[2]._js)
|
||||
select.options.add(
|
||||
value="2a", html="Option 2a", before=select.options[2]._dom_element
|
||||
)
|
||||
|
||||
# EXPECT the select element to have 3 options
|
||||
assert len(select.options) == 5
|
||||
|
||||
# EXPECT the middle option to have the value and html we passed in
|
||||
assert select.options[0].value == "1"
|
||||
assert select.options[0].html == "Option 1"
|
||||
assert select.options[0].innerHTML == "Option 1"
|
||||
assert select.options[1].value == "2"
|
||||
assert select.options[1].html == "Option 2"
|
||||
assert select.options[1].innerHTML == "Option 2"
|
||||
assert select.options[2].value == "2a"
|
||||
assert select.options[2].html == "Option 2a"
|
||||
assert select.options[2].innerHTML == "Option 2a"
|
||||
assert select.options[3].value == "3"
|
||||
assert select.options[3].html == "Option 3"
|
||||
assert select.options[3].innerHTML == "Option 3"
|
||||
assert select.options[4].value == ""
|
||||
assert select.options[4].html == ""
|
||||
assert select.options[4].innerHTML == ""
|
||||
|
||||
def test_select_options_remove(self):
|
||||
# GIVEN the existing select element with 3 options
|
||||
select = pydom[f"#test_select_element_to_remove"][0]
|
||||
select = page.find(f"#test_select_element_to_remove")[0]
|
||||
|
||||
# EXPECT the select element to have 3 options
|
||||
assert len(select.options) == 4
|
||||
@@ -448,12 +449,12 @@ class TestSelect:
|
||||
|
||||
def test_select_get_selected_option(self):
|
||||
# GIVEN the existing select element with one selected option
|
||||
select = pydom[f"#test_select_element_w_options"][0]
|
||||
select = page.find(f"#test_select_element_w_options")[0]
|
||||
|
||||
# WHEN we get the selected option
|
||||
selected_option = select.options.selected
|
||||
|
||||
# EXPECT the selected option to be correct
|
||||
assert selected_option.value == "2"
|
||||
assert selected_option.html == "Option 2"
|
||||
assert selected_option.selected == selected_option._js.selected == True
|
||||
assert selected_option.innerHTML == "Option 2"
|
||||
assert selected_option.selected == selected_option._dom_element.selected == True
|
||||
|
||||
16
pyscript.core/test/service-worker/index.html
Normal file
16
pyscript.core/test/service-worker/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Service Worker</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" service-worker="./sabayon.js" worker>
|
||||
from pyscript import document
|
||||
document.body.append('OK')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
28
pyscript.core/test/service-worker/mini-coi.js
Normal file
28
pyscript.core/test/service-worker/mini-coi.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
|
||||
/*! mini-coi - Andrea Giammarchi and contributors, licensed under MIT */
|
||||
(({ document: d, navigator: { serviceWorker: s } }) => {
|
||||
if (d) {
|
||||
const { currentScript: c } = d;
|
||||
s.register(c.src, { scope: c.getAttribute('scope') || '.' }).then(r => {
|
||||
r.addEventListener('updatefound', () => location.reload());
|
||||
if (r.active && !s.controller) location.reload();
|
||||
});
|
||||
}
|
||||
else {
|
||||
addEventListener('install', () => skipWaiting());
|
||||
addEventListener('activate', e => e.waitUntil(clients.claim()));
|
||||
addEventListener('fetch', e => {
|
||||
const { request: r } = e;
|
||||
if (r.cache === 'only-if-cached' && r.mode !== 'same-origin') return;
|
||||
e.respondWith(fetch(r).then(r => {
|
||||
const { body, status, statusText } = r;
|
||||
if (!status || status > 399) return r;
|
||||
const h = new Headers(r.headers);
|
||||
h.set('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
h.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||
h.set('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
return new Response(body, { status, statusText, headers: h });
|
||||
}));
|
||||
});
|
||||
}
|
||||
})(self);
|
||||
1
pyscript.core/test/service-worker/sabayon.js
Normal file
1
pyscript.core/test/service-worker/sabayon.js
Normal file
@@ -0,0 +1 @@
|
||||
const{isArray:e}=Array,t=new Map,s=e=>{e.stopImmediatePropagation(),e.preventDefault()};var n=Object.freeze({__proto__:null,activate:e=>e.waitUntil(clients.claim()),fetch:e=>{const{request:n}=e;"POST"===n.method&&n.url===`${location.href}?sabayon`&&(s(e),e.respondWith(n.json().then((async e=>{const{promise:s,resolve:o}=Promise.withResolvers(),a=e.join(",");t.set(a,o);for(const t of await clients.matchAll())t.postMessage(e);return s.then((e=>new Response(`[${e.join(",")}]`,n.headers)))}))))},install:()=>skipWaiting(),message:n=>{const{data:o}=n;if(e(o)&&4===o.length){const[e,a,i,r]=o,l=[e,a,i].join(",");t.has(l)&&(s(n),t.get(l)(r),t.delete(l))}}});for(const e in n)addEventListener(e,n[e]);
|
||||
46
pyscript.core/test/storage.html
Normal file
46
pyscript.core/test/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>
|
||||
251
pyscript.core/test/ui/demo.py
Normal file
251
pyscript.core/test/ui/demo.py
Normal file
@@ -0,0 +1,251 @@
|
||||
try:
|
||||
from textwrap import dedent
|
||||
except ImportError:
|
||||
dedent = lambda x: x
|
||||
|
||||
import examples
|
||||
import shoelace
|
||||
import styles
|
||||
from markdown import markdown
|
||||
from pyscript import when, window
|
||||
from pyweb import pydom
|
||||
from pyweb.ui import elements as el
|
||||
from pyweb.ui.elements import a, button, div, grid, h1, h2, h3
|
||||
|
||||
MAIN_PAGE_MARKDOWN = dedent(
|
||||
"""
|
||||
## What is pyweb.ui?
|
||||
Pyweb UI is a totally immagnary exercise atm but..... imagine it is a Python library that allows you to create
|
||||
web applications using Python only.
|
||||
|
||||
It is based on base HTML/JS components but is extensible, for instance, it can have a [Shoelace](https://shoelace.style/) backend...
|
||||
|
||||
PyWeb is a Python library that allows you to create web applications using Python only.
|
||||
|
||||
## What can I do with Pyweb.ui?
|
||||
|
||||
You can create web applications using Python only.
|
||||
"""
|
||||
)
|
||||
|
||||
# First thing we do is to load all the external resources we need
|
||||
shoelace.load_resources()
|
||||
|
||||
|
||||
# Let's define some convenience functions first
|
||||
def create_component_details(component_label, component):
|
||||
"""Create a component details card.
|
||||
|
||||
Args:
|
||||
component (str): The name of the component to create.
|
||||
|
||||
Returns:
|
||||
the component created
|
||||
|
||||
"""
|
||||
# Get the example from the examples catalog
|
||||
example = component["instance"]
|
||||
details = (
|
||||
getattr(example, "__doc__", "")
|
||||
or f"Details missing for component {component_label}"
|
||||
)
|
||||
|
||||
return div(
|
||||
[
|
||||
# Title and description (description is picked from the class docstring)
|
||||
h1(component_label),
|
||||
markdown(details),
|
||||
# Example section
|
||||
h2("Example:"),
|
||||
create_component_example(component["instance"], component["code"]),
|
||||
],
|
||||
style={"margin": "20px"},
|
||||
)
|
||||
|
||||
|
||||
def add_component_section(component_label, component, parent_div):
|
||||
"""Create a link to a component and add it to the left panel.
|
||||
|
||||
Args:
|
||||
component (str): The name of the component to add.
|
||||
|
||||
Returns:
|
||||
the component created
|
||||
|
||||
"""
|
||||
# Create the component link element
|
||||
div_ = div(
|
||||
a(component_label, href="#"),
|
||||
style={"display": "block", "text-align": "center", "margin": "auto"},
|
||||
)
|
||||
|
||||
# Create a handler that opens the component details when the link is clicked
|
||||
@when("click", div_)
|
||||
def _change():
|
||||
new_main = create_component_details(component_label, component)
|
||||
main_area.html = ""
|
||||
main_area.append(new_main)
|
||||
|
||||
# Add the new link element to the parent div (left panel)
|
||||
parent_div.append(div_)
|
||||
return div_
|
||||
|
||||
|
||||
def create_component_example(widget, code):
|
||||
"""Create a grid div with the widget on the left side and the relate code
|
||||
on the right side.
|
||||
|
||||
Args:
|
||||
widget (ElementBase): The widget to add to the grid.
|
||||
code (str): The code to add to the grid.
|
||||
|
||||
Returns:
|
||||
the grid created
|
||||
|
||||
"""
|
||||
# Create the grid that splits the window in two columns (25% and 75%)
|
||||
grid_ = grid("29% 2% 74%")
|
||||
|
||||
# Add the widget
|
||||
grid_.append(div(widget, style=styles.STYLE_EXAMPLE_INSTANCE))
|
||||
|
||||
# Add the code div
|
||||
widget_code = markdown(dedent(f"""```python\n{code}\n```"""))
|
||||
grid_.append(shoelace.Divider(vertical=True))
|
||||
grid_.append(div(widget_code, style=styles.STYLE_CODE_BLOCK))
|
||||
|
||||
return grid_
|
||||
|
||||
|
||||
def create_main_area():
|
||||
"""Create the main area of the right side of page, with the description of the
|
||||
demo itself and how to use it.
|
||||
|
||||
Returns:
|
||||
the main area
|
||||
|
||||
"""
|
||||
div_ = div(
|
||||
[
|
||||
h1("Welcome to PyWeb UI!", style={"text-align": "center"}),
|
||||
markdown(MAIN_PAGE_MARKDOWN),
|
||||
]
|
||||
)
|
||||
|
||||
main = el.main(
|
||||
style={
|
||||
"padding-top": "4rem",
|
||||
"padding-bottom": "7rem",
|
||||
"max-width": "52rem",
|
||||
"margin-left": "auto",
|
||||
"margin-right": "auto",
|
||||
"padding-left": "1.5rem",
|
||||
"padding-right": "1.5rem",
|
||||
"width": "100%",
|
||||
}
|
||||
)
|
||||
main.append(div_)
|
||||
|
||||
return main
|
||||
|
||||
|
||||
def create_basic_components_page(label, kit_name):
|
||||
"""Create the basic components page.
|
||||
|
||||
Returns:
|
||||
the main area
|
||||
|
||||
"""
|
||||
div_ = div(h2(label))
|
||||
|
||||
for component_label, component in examples.kits[kit_name].items():
|
||||
div_.append(h3(component_label))
|
||||
div_.append(create_component_example(component["instance"], component["code"]))
|
||||
|
||||
return div_
|
||||
|
||||
|
||||
# ********** CREATE ALL THE LAYOUT **********
|
||||
|
||||
main_grid = grid("140px 20px auto", style={"min-height": "100%"})
|
||||
|
||||
# ********** MAIN PANEL **********
|
||||
main_area = create_main_area()
|
||||
|
||||
|
||||
def write_to_main(content):
|
||||
main_area.html = ""
|
||||
main_area.append(content)
|
||||
|
||||
|
||||
def restore_home():
|
||||
write_to_main(create_main_area())
|
||||
|
||||
|
||||
def basic_components():
|
||||
write_to_main(
|
||||
create_basic_components_page(label="Basic Components", kit_name="elements")
|
||||
)
|
||||
# Make sure we highlight the code
|
||||
window.hljs.highlightAll()
|
||||
|
||||
|
||||
def markdown_components():
|
||||
write_to_main(create_basic_components_page(label="", kit_name="markdown"))
|
||||
|
||||
|
||||
def create_new_section(title, parent_div):
|
||||
basic_components_text = h3(
|
||||
title, style={"text-align": "left", "margin": "20px auto 0"}
|
||||
)
|
||||
parent_div.append(basic_components_text)
|
||||
parent_div.append(
|
||||
shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"})
|
||||
)
|
||||
return basic_components_text
|
||||
|
||||
|
||||
# ********** LEFT PANEL **********
|
||||
left_div = div()
|
||||
left_panel_title = h1(
|
||||
"PyWeb.UI", style={"text-align": "center", "margin": "20px auto 30px"}
|
||||
)
|
||||
left_div.append(left_panel_title)
|
||||
left_div.append(shoelace.Divider(style={"margin-bottom": "30px"}))
|
||||
# Let's map the creation of the main area to when the user clocks on "Components"
|
||||
when("click", left_panel_title)(restore_home)
|
||||
|
||||
# BASIC COMPONENTS
|
||||
basic_components_text = h3(
|
||||
"Basic Components",
|
||||
style={"text-align": "left", "margin": "20px auto 0", "cursor": "pointer"},
|
||||
)
|
||||
left_div.append(basic_components_text)
|
||||
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||
# Let's map the creation of the main area to when the user clocks on "Components"
|
||||
when("click", basic_components_text)(basic_components)
|
||||
|
||||
# MARKDOWN COMPONENTS
|
||||
markdown_title = create_new_section("Markdown", left_div)
|
||||
when("click", markdown_title)(markdown_components)
|
||||
|
||||
|
||||
# SHOELACE COMPONENTS
|
||||
shoe_components_text = h3(
|
||||
"Shoe Components", style={"text-align": "left", "margin": "20px auto 0"}
|
||||
)
|
||||
left_div.append(shoe_components_text)
|
||||
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||
|
||||
# Create the links to the components on th left panel
|
||||
print("SHOELACE EXAMPLES", examples.kits["shoelace"])
|
||||
for component_label, component in examples.kits["shoelace"].items():
|
||||
add_component_section(component_label, component, left_div)
|
||||
|
||||
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||
left_div.append(a("Gallery", href="gallery.html", style={"text-align": "left"}))
|
||||
# ********** ADD LEFT AND MAIN PANEL TO MAIN **********
|
||||
main_grid.append(left_div)
|
||||
main_grid.append(shoelace.Divider(vertical=True))
|
||||
main_grid.append(main_area)
|
||||
pydom.body.append(main_grid)
|
||||
300
pyscript.core/test/ui/examples.py
Normal file
300
pyscript.core/test/ui/examples.py
Normal file
@@ -0,0 +1,300 @@
|
||||
from markdown import markdown
|
||||
from pyscript import when, window
|
||||
from pyweb import pydom
|
||||
from pyweb.ui.elements import (
|
||||
a,
|
||||
br,
|
||||
button,
|
||||
code,
|
||||
div,
|
||||
grid,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
img,
|
||||
input_,
|
||||
p,
|
||||
small,
|
||||
strong,
|
||||
)
|
||||
from shoelace import (
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
CopyButton,
|
||||
Details,
|
||||
Dialog,
|
||||
Divider,
|
||||
Icon,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Range,
|
||||
Rating,
|
||||
RelativeTime,
|
||||
Skeleton,
|
||||
Spinner,
|
||||
Switch,
|
||||
Tag,
|
||||
Textarea,
|
||||
)
|
||||
|
||||
LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
details_code = """
|
||||
LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
Details(LOREM_IPSUM, summary="Try me")
|
||||
"""
|
||||
example_dialog_close_btn = Button("Close")
|
||||
example_dialog = Dialog(div([p(LOREM_IPSUM), example_dialog_close_btn]), label="Try me")
|
||||
example_dialog_btn = Button("Open Dialog")
|
||||
|
||||
|
||||
def toggle_dialog():
|
||||
example_dialog.open = not (example_dialog.open)
|
||||
|
||||
|
||||
when("click", example_dialog_btn)(toggle_dialog)
|
||||
when("click", example_dialog_close_btn)(toggle_dialog)
|
||||
|
||||
pydom.body.append(example_dialog)
|
||||
|
||||
|
||||
# ELEMENTS
|
||||
|
||||
# Button
|
||||
btn = button("Click me!")
|
||||
when("click", btn)(lambda: window.alert("Clicked!"))
|
||||
|
||||
# Inputs
|
||||
inputs_div = div()
|
||||
inputs_code = []
|
||||
for input_type in [
|
||||
"text",
|
||||
"password",
|
||||
"email",
|
||||
"number",
|
||||
"date",
|
||||
"time",
|
||||
"color",
|
||||
"range",
|
||||
]:
|
||||
inputs_div.append(input_(type=input_type, style={"display": "block"}))
|
||||
inputs_code.append(f"input_(type='{input_type}')")
|
||||
|
||||
|
||||
headers_div = div()
|
||||
headers_code = []
|
||||
for header in [h1, h2, h3, h4, h5, h6]:
|
||||
headers_div.append(header(f"{header.tag.upper()} header"))
|
||||
headers_code.append(f'{header.tag}("{header.tag.upper()} header")')
|
||||
headers_code = "\n".join(headers_code)
|
||||
|
||||
rich_input = input_(
|
||||
type="text",
|
||||
name="some name",
|
||||
autofocus=True,
|
||||
pattern="\w{3,16}",
|
||||
placeholder="add text with > 3 chars",
|
||||
required=True,
|
||||
size="20",
|
||||
)
|
||||
inputs_div.append(rich_input)
|
||||
inputs_code.append("# You can create inputs with more options like")
|
||||
inputs_code.append("# this by passing properties as kwargs")
|
||||
inputs_code.append(
|
||||
"input_(type='text', name='some name', autofocus=True, pattern='\\w{3,16}', placeholder='add text with > 3 chars', required=True, size='20')"
|
||||
)
|
||||
inputs_code = "\n".join(inputs_code)
|
||||
|
||||
MARKDOWN_EXAMPLE = """# This is a header
|
||||
|
||||
This is a ~~paragraph~~ text with **bold** and *italic* text in it!
|
||||
"""
|
||||
|
||||
kits = {
|
||||
"shoelace": {
|
||||
"Alert": {
|
||||
"instance": Alert(
|
||||
"This is a standard alert. You can customize its content and even the icon."
|
||||
),
|
||||
"code": "Alert('This is a standard alert. You can customize its content and even the icon.'",
|
||||
},
|
||||
"Icon": {
|
||||
"instance": Icon(name="heart"),
|
||||
"code": 'Icon(name="heart")',
|
||||
},
|
||||
"Button": {
|
||||
"instance": Button("Try me"),
|
||||
"code": 'Button("Try me")',
|
||||
},
|
||||
"Card": {
|
||||
"instance": Card(
|
||||
p("This is a cool card!"),
|
||||
image="https://pyscript.net/assets/images/pyscript-sticker-black.svg",
|
||||
footer=div([Button("More Info"), Rating()]),
|
||||
),
|
||||
"code": """
|
||||
Card(p("This is a cool card!"), image="https://pyscript.net/assets/images/pyscript-sticker-black.svg", footer=div([Button("More Info"), Rating()]))
|
||||
""",
|
||||
},
|
||||
"Details": {
|
||||
"instance": Details(LOREM_IPSUM, summary="Try me"),
|
||||
"code": 'Details(LOREM_IPSUM, summary="Try me")',
|
||||
},
|
||||
"Dialog": {
|
||||
"instance": example_dialog_btn,
|
||||
"code": 'Dialog(div([p(LOREM_IPSUM), Button("Close")]), summary="Try me")',
|
||||
},
|
||||
"Divider": {
|
||||
"instance": Divider(),
|
||||
"code": "Divider()",
|
||||
},
|
||||
"Rating": {
|
||||
"instance": Rating(),
|
||||
"code": "Rating()",
|
||||
},
|
||||
"Radio": {
|
||||
"instance": Radio("Option 42"),
|
||||
"code": code('Radio("Option 42")'),
|
||||
},
|
||||
"Radio Group": {
|
||||
"instance": RadioGroup(
|
||||
[
|
||||
Radio("radio 1", name="radio 1", value=1, style={"margin": "20px"}),
|
||||
Radio("radio 2", name="radio 2", value=2, style={"margin": "20px"}),
|
||||
Radio("radio 3", name="radio 3", value=3, style={"margin": "20px"}),
|
||||
],
|
||||
label="Select an option",
|
||||
),
|
||||
"code": code(
|
||||
"""
|
||||
RadioGroup([Radio("radio 1", name="radio 1", value=1, style={"margin": "20px"}),
|
||||
Radio("radio 2", name="radio 2", value=2, style={"margin": "20px"}),
|
||||
Radio("radio 3", name="radio 3", value=3, style={"margin": "20px"})],
|
||||
label="Select an option"),"""
|
||||
),
|
||||
},
|
||||
"CopyButton": {
|
||||
"instance": CopyButton(
|
||||
value="PyShoes!",
|
||||
copy_label="Copy me!",
|
||||
sucess_label="Copied, check your clipboard!",
|
||||
error_label="Oops, something went wrong!",
|
||||
feedback_timeout=2000,
|
||||
tooltip_placement="top",
|
||||
),
|
||||
"code": 'CopyButton(value="PyShoes!", copy_label="Copy me!", sucess_label="Copied, check your clipboard!", error_label="Oops, something went wrong!", feedback_timeout=2000, tooltip_placement="top")',
|
||||
},
|
||||
"Skeleton": {
|
||||
"instance": Skeleton(effect="pulse"),
|
||||
"code": "Skeleton(effect='pulse')",
|
||||
},
|
||||
"Spinner": {
|
||||
"instance": Spinner(),
|
||||
"code": "Spinner()",
|
||||
},
|
||||
"Switch": {
|
||||
"instance": Switch(name="switch", size="large"),
|
||||
"code": 'Switch(name="switch", size="large")',
|
||||
},
|
||||
"Textarea": {
|
||||
"instance": Textarea(
|
||||
name="textarea",
|
||||
label="Textarea",
|
||||
size="medium",
|
||||
help_text="This is a textarea",
|
||||
resize="auto",
|
||||
),
|
||||
"code": 'Textarea(name="textarea", label="Textarea", size="medium", help_text="This is a textarea", resize="auto")',
|
||||
},
|
||||
"Tag": {
|
||||
"instance": Tag("Tag", variant="primary", size="medium"),
|
||||
"code": 'Tag("Tag", variant="primary", size="medium")',
|
||||
},
|
||||
"Range": {
|
||||
"instance": Range(min=0, max=100, value=50),
|
||||
"code": "Range(min=0, max=100, value=50)",
|
||||
},
|
||||
"RelativeTime": {
|
||||
"instance": RelativeTime(date="2021-01-01T00:00:00Z"),
|
||||
"code": 'RelativeTime(date="2021-01-01T00:00:00Z")',
|
||||
},
|
||||
# "SplitPanel": {
|
||||
# "instance": SplitPanel(
|
||||
# div("First panel"), div("Second panel"), orientation="vertical"
|
||||
# ),
|
||||
# "code": code(
|
||||
# 'SplitPanel(div("First panel"), div("Second panel"), orientation="vertical")'
|
||||
# ),
|
||||
# },
|
||||
},
|
||||
"elements": {
|
||||
"button": {
|
||||
"instance": btn,
|
||||
"code": """btn = button("Click me!")
|
||||
when('click', btn)(lambda: window.alert("Clicked!"))
|
||||
parentdiv.append(btn)
|
||||
""",
|
||||
},
|
||||
"div": {
|
||||
"instance": div(
|
||||
"This is a div",
|
||||
style={
|
||||
"text-align": "center",
|
||||
"margin": "0 auto",
|
||||
"background-color": "cornsilk",
|
||||
},
|
||||
),
|
||||
"code": 'div("This is a div", style={"text-align": "center", "margin": "0 auto", "background-color": "cornsilk"})',
|
||||
},
|
||||
"input": {"instance": inputs_div, "code": inputs_code},
|
||||
"grid": {
|
||||
"instance": grid(
|
||||
"30% 70%",
|
||||
[
|
||||
div("This is a grid", style={"background-color": "lightblue"}),
|
||||
p("with 2 elements", style={"background-color": "lightyellow"}),
|
||||
],
|
||||
),
|
||||
"code": 'grid([div("This is a grid")])',
|
||||
},
|
||||
"headers": {"instance": headers_div, "code": headers_code},
|
||||
"a": {
|
||||
"instance": a(
|
||||
"Click here for something awesome",
|
||||
href="https://pyscript.net",
|
||||
target="_blank",
|
||||
),
|
||||
"code": 'a("Click here for something awesome", href="https://pyscript.net", target="_blank")',
|
||||
},
|
||||
"br": {
|
||||
"instance": div([p("This is a paragraph"), br(), p("with a line break")]),
|
||||
"code": 'div([p("This is a paragraph"), br(), p("with a line break")])',
|
||||
},
|
||||
"img": {
|
||||
"instance": img(src="./giphy_winner.gif", style={"max-width": "200px"}),
|
||||
"code": 'img(src="./giphy_winner.gif", style={"max-width": "200px"})',
|
||||
},
|
||||
"code": {
|
||||
"instance": code("print('Hello, World!')"),
|
||||
"code": "code(\"print('Hello, World!')\")",
|
||||
},
|
||||
"p": {"instance": p("This is a paragraph"), "code": 'p("This is a paragraph")'},
|
||||
"small": {
|
||||
"instance": small("This is a small text"),
|
||||
"code": 'small("This is a small text")',
|
||||
},
|
||||
"strong": {
|
||||
"instance": strong("This is a strong text"),
|
||||
"code": 'strong("This is a strong text")',
|
||||
},
|
||||
},
|
||||
"markdown": {
|
||||
"markdown": {
|
||||
"instance": markdown(MARKDOWN_EXAMPLE),
|
||||
"code": f'markdown("""{MARKDOWN_EXAMPLE}""")',
|
||||
},
|
||||
},
|
||||
}
|
||||
31
pyscript.core/test/ui/gallery.html
Normal file
31
pyscript.core/test/ui/gallery.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>PyDom UI</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/lib/marked.umd.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
|
||||
<!-- and it's easy to individually load additional languages -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
|
||||
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="mpy" src="./gallery.py" config="./pyscript.toml"></script>
|
||||
</body>
|
||||
</html>
|
||||
180
pyscript.core/test/ui/gallery.py
Normal file
180
pyscript.core/test/ui/gallery.py
Normal file
@@ -0,0 +1,180 @@
|
||||
try:
|
||||
from textwrap import dedent
|
||||
except ImportError:
|
||||
dedent = lambda x: x
|
||||
|
||||
import inspect
|
||||
|
||||
import shoelace
|
||||
import styles
|
||||
import tictactoe
|
||||
from markdown import markdown
|
||||
from pyscript import when, window
|
||||
from pyweb import pydom
|
||||
from pyweb.ui import elements as el
|
||||
|
||||
MAIN_PAGE_MARKDOWN = dedent(
|
||||
"""
|
||||
This gallery is a collection of demos using the PyWeb.UI library. There are meant
|
||||
to be examples of how to use the library to create GUI applications using Python
|
||||
only.
|
||||
|
||||
## How to use the gallery
|
||||
|
||||
Simply click on the demo you want to see and the details will appear on the right
|
||||
"""
|
||||
)
|
||||
|
||||
# First thing we do is to load all the external resources we need
|
||||
shoelace.load_resources()
|
||||
|
||||
|
||||
def add_demo(demo_name, demo_creator_cb, parent_div, source=None):
|
||||
"""Create a link to a component and add it to the left panel.
|
||||
|
||||
Args:
|
||||
component (str): The name of the component to add.
|
||||
|
||||
Returns:
|
||||
the component created
|
||||
|
||||
"""
|
||||
# Create the component link element
|
||||
div = el.div(el.a(demo_name, href="#"), style=styles.STYLE_LEFT_PANEL_LINKS)
|
||||
|
||||
# Create a handler that opens the component details when the link is clicked
|
||||
@when("click", div)
|
||||
def _change():
|
||||
if source:
|
||||
demo_div = el.grid("50% 50%")
|
||||
demo_div.append(demo_creator_cb())
|
||||
widget_code = markdown(dedent(f"""```python\n{source}\n```"""))
|
||||
demo_div.append(el.div(widget_code, style=styles.STYLE_CODE_BLOCK))
|
||||
else:
|
||||
demo_div = demo_creator_cb()
|
||||
demo_div.style["margin"] = "20px"
|
||||
write_to_main(demo_div)
|
||||
window.hljs.highlightAll()
|
||||
|
||||
# Add the new link element to the parent div (left panel)
|
||||
parent_div.append(div)
|
||||
return div
|
||||
|
||||
|
||||
def create_main_area():
|
||||
"""Create the main area of the right side of page, with the description of the
|
||||
demo itself and how to use it.
|
||||
|
||||
Returns:
|
||||
the main area
|
||||
|
||||
"""
|
||||
return el.div(
|
||||
[
|
||||
el.h1("PyWeb UI Gallery", style={"text-align": "center"}),
|
||||
markdown(MAIN_PAGE_MARKDOWN),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def create_markdown_app():
|
||||
"""Create the basic components page.
|
||||
|
||||
Returns:
|
||||
the main area
|
||||
|
||||
"""
|
||||
translate_button = shoelace.Button("Convert", variant="primary")
|
||||
markdown_txt_area = shoelace.TextArea(label="Use this to write your Markdown")
|
||||
result_div = el.div(style=styles.STYLE_MARKDOWN_RESULT)
|
||||
|
||||
@when("click", translate_button)
|
||||
def translate_markdown():
|
||||
result_div.html = markdown(markdown_txt_area.value).html
|
||||
|
||||
return el.div(
|
||||
[
|
||||
el.h2("Markdown"),
|
||||
markdown_txt_area,
|
||||
translate_button,
|
||||
result_div,
|
||||
],
|
||||
style={"margin": "20px"},
|
||||
)
|
||||
|
||||
|
||||
# ********** MAIN PANEL **********
|
||||
main_area = create_main_area()
|
||||
|
||||
|
||||
def write_to_main(content):
|
||||
main_area.html = ""
|
||||
main_area.append(content)
|
||||
|
||||
|
||||
def restore_home():
|
||||
write_to_main(create_main_area())
|
||||
|
||||
|
||||
def create_new_section(title, parent_div):
|
||||
basic_components_text = el.h3(
|
||||
title, style={"text-align": "left", "margin": "20px auto 0"}
|
||||
)
|
||||
parent_div.append(basic_components_text)
|
||||
parent_div.append(
|
||||
shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"})
|
||||
)
|
||||
return basic_components_text
|
||||
|
||||
|
||||
# ********** LEFT PANEL **********
|
||||
left_panel_title = el.h1("PyWeb.UI", style=styles.STYLE_LEFT_PANEL_TITLE)
|
||||
left_div = el.div(
|
||||
[
|
||||
left_panel_title,
|
||||
shoelace.Divider(style={"margin-bottom": "30px"}),
|
||||
el.h3("Demos", style=styles.STYLE_LEFT_PANEL_TITLE),
|
||||
]
|
||||
)
|
||||
|
||||
# Let's map the creation of the main area to when the user clocks on "Components"
|
||||
when("click", left_panel_title)(restore_home)
|
||||
|
||||
# ------ ADD DEMOS ------
|
||||
markdown_source = """
|
||||
translate_button = shoelace.Button("Convert", variant="primary")
|
||||
markdown_txt_area = shoelace.TextArea(label="Markdown",
|
||||
help_text="Write your Mardown here and press convert to see the result",
|
||||
)
|
||||
result_div = el.div(style=styles.STYLE_MARKDOWN_RESULT)
|
||||
@when("click", translate_button)
|
||||
def translate_markdown():
|
||||
result_div.html = markdown(markdown_txt_area.value).html
|
||||
|
||||
el.div([
|
||||
el.h2("Markdown"),
|
||||
markdown_txt_area,
|
||||
translate_button,
|
||||
result_div,
|
||||
])
|
||||
"""
|
||||
add_demo("Markdown", create_markdown_app, left_div, source=markdown_source)
|
||||
add_demo(
|
||||
"Tic Tac Toe",
|
||||
tictactoe.create_tic_tac_toe,
|
||||
left_div,
|
||||
source=inspect.getsource(tictactoe),
|
||||
)
|
||||
|
||||
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
|
||||
left_div.append(el.a("Examples", href="/test/ui/", style={"text-align": "left"}))
|
||||
|
||||
# ********** CREATE ALL THE LAYOUT **********
|
||||
grid = el.grid("minmax(100px, 200px) 20px auto", style={"min-height": "100%"})
|
||||
grid.append(left_div)
|
||||
grid.append(shoelace.Divider(vertical=True))
|
||||
grid.append(main_area)
|
||||
|
||||
pydom.body.append(grid)
|
||||
pydom.body.append(el.a("Back to the main page", href="/test/ui/", target="_blank"))
|
||||
pydom.body.append(el.a("Hidden!!!", href="/test/ui/", target="_blank", hidden=True))
|
||||
39
pyscript.core/test/ui/index.html
Normal file
39
pyscript.core/test/ui/index.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>PyDom UI</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/lib/marked.umd.min.js"></script>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
|
||||
<!-- and it's easy to individually load additional languages -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
|
||||
<!-- SHOWLACE CUSTOM CSS -->
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
|
||||
}
|
||||
|
||||
input:invalid {
|
||||
background-color: lightpink;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="mpy" src="./demo.py" config="./pyscript.toml"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
pyscript.core/test/ui/pyscript.toml
Normal file
8
pyscript.core/test/ui/pyscript.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
packages = []
|
||||
|
||||
[files]
|
||||
"./examples.py" = "./examples.py"
|
||||
"./tictactoe.py" = "./tictactoe.py"
|
||||
"./styles.py" = "./styles.py"
|
||||
"./shoelace.py" = "./shoelace.py"
|
||||
"./markdown.py" = "./markdown.py"
|
||||
2
pyscript.core/test/workers/config.toml
Normal file
2
pyscript.core/test/workers/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[files]
|
||||
"./test.py" = "./test.py"
|
||||
30
pyscript.core/test/workers/index.html
Normal file
30
pyscript.core/test/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/test/workers/named.html
Normal file
29
pyscript.core/test/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/test/workers/test.py
Normal file
19
pyscript.core/test/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/test/workers/worker.py
Normal file
7
pyscript.core/test/workers/worker.py
Normal file
@@ -0,0 +1,7 @@
|
||||
def micropython_version():
|
||||
import sys
|
||||
|
||||
return sys.version
|
||||
|
||||
|
||||
__export__ = ["micropython_version"]
|
||||
6
pyscript.core/test/ws.spec.js
Normal file
6
pyscript.core/test/ws.spec.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('MicroPython WebSocket', async ({ page }) => {
|
||||
await page.goto('http://localhost:5037/');
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
33
pyscript.core/test/ws/index.html
Normal file
33
pyscript.core/test/ws/index.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" worker>
|
||||
from pyscript import WebSocket, document
|
||||
|
||||
def onopen(event):
|
||||
print(event.type)
|
||||
ws.send("hello")
|
||||
|
||||
def onmessage(event):
|
||||
print(event.type, event.data)
|
||||
ws.close()
|
||||
|
||||
def onclose(event):
|
||||
print(event.type)
|
||||
document.documentElement.classList.add("ok")
|
||||
|
||||
ws = WebSocket(
|
||||
url="ws://localhost:5037/",
|
||||
onopen=onopen,
|
||||
onmessage=onmessage,
|
||||
onclose=onclose
|
||||
)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
33
pyscript.core/test/ws/index.js
Normal file
33
pyscript.core/test/ws/index.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { serve, file } from 'bun';
|
||||
|
||||
import path, { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const dir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
serve({
|
||||
port: 5037,
|
||||
fetch(req, server) {
|
||||
if (server.upgrade(req)) return;
|
||||
const url = new URL(req.url);
|
||||
let { pathname } = url;
|
||||
if (pathname === '/') pathname = '/index.html';
|
||||
else if (/^\/dist\//.test(pathname)) pathname = `/../..${pathname}`;
|
||||
else if (pathname === '/favicon.ico')
|
||||
return new Response('Not Found', { status: 404 });
|
||||
const response = new Response(file(`${dir}${pathname}`));
|
||||
const { headers } = response;
|
||||
headers.set('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||
headers.set('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
return response;
|
||||
},
|
||||
websocket: {
|
||||
message(ws, message) {
|
||||
ws.send(message);
|
||||
},
|
||||
close() {
|
||||
process.exit(0);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -186,12 +186,12 @@ class TestSupport(PyScriptTest):
|
||||
#
|
||||
msg = str(exc.value)
|
||||
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
|
||||
|
||||
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/types/config.d.ts
vendored
4
pyscript.core/types/config.d.ts
vendored
@@ -1,2 +1,2 @@
|
||||
export default configs;
|
||||
declare const configs: Map<any, any>;
|
||||
export const configs: Map<any, any>;
|
||||
export function relative_url(url: any, base?: string): string;
|
||||
|
||||
7
pyscript.core/types/core.d.ts
vendored
7
pyscript.core/types/core.d.ts
vendored
@@ -1,6 +1,9 @@
|
||||
export function offline_interpreter(config: any): string;
|
||||
import { stdlib } from "./stdlib.js";
|
||||
import { optional } from "./stdlib.js";
|
||||
import { inputFailure } from "./hooks.js";
|
||||
import TYPES from "./types.js";
|
||||
import { relative_url } from "./config.js";
|
||||
/**
|
||||
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||
* @param {string} file the python file to run ina worker.
|
||||
@@ -51,5 +54,5 @@ declare const exportedHooks: {
|
||||
};
|
||||
};
|
||||
declare const exportedConfig: {};
|
||||
declare const exportedWhenDefined: (type: string) => Promise<any>;
|
||||
export { stdlib, optional, TYPES, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
||||
declare const exportedWhenDefined: (type: string) => Promise<object>;
|
||||
export { stdlib, optional, inputFailure, TYPES, relative_url, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
||||
|
||||
17
pyscript.core/types/exceptions.d.ts
vendored
17
pyscript.core/types/exceptions.d.ts
vendored
@@ -53,19 +53,4 @@ export class InstallError extends UserError {
|
||||
/**
|
||||
* Keys of the ErrorCode object
|
||||
*/
|
||||
export type ErrorCodes = keyof {
|
||||
GENERIC: string;
|
||||
CONFLICTING_CODE: string;
|
||||
BAD_CONFIG: string;
|
||||
MICROPIP_INSTALL_ERROR: string;
|
||||
BAD_PLUGIN_FILE_EXTENSION: string;
|
||||
NO_DEFAULT_EXPORT: string;
|
||||
TOP_LEVEL_AWAIT: string;
|
||||
FETCH_ERROR: string;
|
||||
FETCH_NAME_ERROR: string;
|
||||
FETCH_UNAUTHORIZED_ERROR: string;
|
||||
FETCH_FORBIDDEN_ERROR: string;
|
||||
FETCH_NOT_FOUND_ERROR: string;
|
||||
FETCH_SERVER_ERROR: string;
|
||||
FETCH_UNAVAILABLE_ERROR: string;
|
||||
};
|
||||
export type ErrorCodes = "GENERIC" | "CONFLICTING_CODE" | "BAD_CONFIG" | "MICROPIP_INSTALL_ERROR" | "BAD_PLUGIN_FILE_EXTENSION" | "NO_DEFAULT_EXPORT" | "TOP_LEVEL_AWAIT" | "FETCH_ERROR" | "FETCH_NAME_ERROR" | "FETCH_UNAUTHORIZED_ERROR" | "FETCH_FORBIDDEN_ERROR" | "FETCH_NOT_FOUND_ERROR" | "FETCH_SERVER_ERROR" | "FETCH_UNAVAILABLE_ERROR";
|
||||
|
||||
1
pyscript.core/types/hooks.d.ts
vendored
1
pyscript.core/types/hooks.d.ts
vendored
@@ -2,6 +2,7 @@ export function main(name: any): any;
|
||||
export function worker(name: any): any;
|
||||
export function codeFor(branch: any, type: any): {};
|
||||
export function createFunction(self: any, name: any): any;
|
||||
export const inputFailure: "\n import builtins\n def input(prompt=\"\"):\n raise Exception(\"\\n \".join([\n \"input() doesn't work when PyScript runs in the main thread.\",\n \"Consider using the worker attribute: https://pyscript.github.io/docs/2023.11.2/user-guide/workers/\"\n ]))\n\n builtins.input = input\n del builtins\n del input\n";
|
||||
export namespace hooks {
|
||||
namespace main {
|
||||
let onWorker: Set<Function>;
|
||||
|
||||
2
pyscript.core/types/plugins/py-terminal/mpy.d.ts
vendored
Normal file
2
pyscript.core/types/plugins/py-terminal/mpy.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare function _default(element: any): Promise<void>;
|
||||
export default _default;
|
||||
2
pyscript.core/types/plugins/py-terminal/py.d.ts
vendored
Normal file
2
pyscript.core/types/plugins/py-terminal/py.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare function _default(element: any): Promise<void>;
|
||||
export default _default;
|
||||
10
pyscript.core/types/stdlib/pyscript.d.ts
vendored
10
pyscript.core/types/stdlib/pyscript.d.ts
vendored
@@ -5,13 +5,13 @@ declare namespace _default {
|
||||
"event_handling.py": string;
|
||||
"fetch.py": string;
|
||||
"ffi.py": string;
|
||||
"flatted.py": string;
|
||||
"magic_js.py": string;
|
||||
"storage.py": string;
|
||||
"util.py": string;
|
||||
};
|
||||
let pyweb: {
|
||||
"__init__.py": string;
|
||||
"media.py": string;
|
||||
"pydom.py": string;
|
||||
"web.py": string;
|
||||
"websocket.py": string;
|
||||
"workers.py": string;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
black==23.11.0
|
||||
isort==5.12.0
|
||||
pytest==7.1.2
|
||||
pre-commit==3.5.0
|
||||
playwright==1.33.0
|
||||
pytest-playwright==0.3.3
|
||||
pytest-xdist==3.3.0
|
||||
black==24.4.2
|
||||
isort==5.13.2
|
||||
pytest==8.2.1
|
||||
pre-commit==3.7.1
|
||||
playwright==1.44.0
|
||||
pytest-playwright==0.5.0
|
||||
pytest-xdist==3.6.1
|
||||
pexpect==4.9.0
|
||||
pyodide_py==0.24.1
|
||||
micropip==0.5.0
|
||||
toml==0.10.2
|
||||
numpy==1.26.2
|
||||
pillow==10.1.0
|
||||
numpy==1.26.4
|
||||
pillow==10.3.0
|
||||
|
||||
Reference in New Issue
Block a user