mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 10:47:35 -05:00
Compare commits
5 Commits
danyeaw-ad
...
when-updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58c91b941b | ||
|
|
b33661ff8e | ||
|
|
9db8b13d9c | ||
|
|
3003a9671d | ||
|
|
b87c86f266 |
@@ -25,29 +25,22 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 25.1.0
|
rev: 24.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
exclude: core/tests
|
|
||||||
args: ["-l", "88", "--skip-string-normalization"]
|
args: ["-l", "88", "--skip-string-normalization"]
|
||||||
|
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.4.1
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell # See 'pyproject.toml' for args
|
- id: codespell # See 'pyproject.toml' for args
|
||||||
exclude: fs\.py|\.js\.map$
|
exclude: \.js\.map$
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- tomli
|
- tomli
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
||||||
rev: v0.9.6
|
|
||||||
hooks:
|
|
||||||
- id: ruff
|
|
||||||
exclude: core/tests
|
|
||||||
|
|
||||||
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
|
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
|
||||||
rev: "v3.0.0-alpha.6"
|
rev: "v3.0.0-alpha.6"
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
exclude: core/tests|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
|
exclude: core/test|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
|
||||||
args: [--tab-width, "4"]
|
args: [--tab-width, "4"]
|
||||||
|
|||||||
9
Makefile
9
Makefile
@@ -40,7 +40,7 @@ check-python:
|
|||||||
|
|
||||||
# Check the environment, install the dependencies.
|
# Check the environment, install the dependencies.
|
||||||
setup: check-node check-npm check-python
|
setup: check-node check-npm check-python
|
||||||
cd core && npm ci && cd ..
|
cd core && npm install && cd ..
|
||||||
ifeq ($(VIRTUAL_ENV),)
|
ifeq ($(VIRTUAL_ENV),)
|
||||||
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
|
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
|
||||||
false
|
false
|
||||||
@@ -55,11 +55,12 @@ clean:
|
|||||||
rm -rf .pytest_cache .coverage coverage.xml
|
rm -rf .pytest_cache .coverage coverage.xml
|
||||||
|
|
||||||
# Build PyScript.
|
# Build PyScript.
|
||||||
build: precommit-check
|
build:
|
||||||
cd core && npx playwright install chromium && npm run build
|
cd core && npx playwright install chromium && npm run build
|
||||||
|
|
||||||
# Update the dependencies.
|
# Update the dependencies.
|
||||||
update:
|
update:
|
||||||
|
cd core && npm update && cd ..
|
||||||
python -m pip install -r requirements.txt --upgrade
|
python -m pip install -r requirements.txt --upgrade
|
||||||
|
|
||||||
# Run the precommit checks (run eslint).
|
# Run the precommit checks (run eslint).
|
||||||
@@ -70,10 +71,6 @@ precommit-check:
|
|||||||
test:
|
test:
|
||||||
cd core && npm run test:integration
|
cd core && npm run test:integration
|
||||||
|
|
||||||
# Serve the repository with the correct headers.
|
|
||||||
serve:
|
|
||||||
npx mini-coi .
|
|
||||||
|
|
||||||
# Format the code.
|
# Format the code.
|
||||||
fmt: fmt-py
|
fmt: fmt-py
|
||||||
@echo "Format completed"
|
@echo "Format completed"
|
||||||
|
|||||||
@@ -83,12 +83,3 @@ documentation for more information on how to setup your development environment.
|
|||||||
|
|
||||||
The [PyScript organization governance](https://github.com/pyscript/governance)
|
The [PyScript organization governance](https://github.com/pyscript/governance)
|
||||||
is documented in a separate repository.
|
is documented in a separate repository.
|
||||||
|
|
||||||
## Supporters
|
|
||||||
|
|
||||||
PyScript is an independent open source project.
|
|
||||||
|
|
||||||
However, PyScript was born at [Anaconda Inc](https://anaconda.com/) and its
|
|
||||||
core contributors are currently employed by Anaconda to work on PyScript. We
|
|
||||||
would like to acknowledge and celebrate Anaconda's continued support of this
|
|
||||||
project. Thank you [Anaconda Inc](https://anaconda.com/)!
|
|
||||||
|
|||||||
924
core/package-lock.json
generated
924
core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pyscript/core",
|
"name": "@pyscript/core",
|
||||||
"version": "0.6.39",
|
"version": "0.6.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "PyScript",
|
"description": "PyScript",
|
||||||
"module": "./index.js",
|
"module": "./index.js",
|
||||||
@@ -25,10 +25,6 @@
|
|||||||
"types": "./types/core.d.ts",
|
"types": "./types/core.d.ts",
|
||||||
"import": "./src/core.js"
|
"import": "./src/core.js"
|
||||||
},
|
},
|
||||||
"./js": {
|
|
||||||
"types": "./types/core.d.ts",
|
|
||||||
"import": "./dist/core.js"
|
|
||||||
},
|
|
||||||
"./css": {
|
"./css": {
|
||||||
"import": "./dist/core.css"
|
"import": "./dist/core.css"
|
||||||
},
|
},
|
||||||
@@ -47,7 +43,7 @@
|
|||||||
"build:3rd-party": "node rollup/3rd-party.cjs",
|
"build:3rd-party": "node rollup/3rd-party.cjs",
|
||||||
"build:tests-index": "node rollup/build_test_index.cjs",
|
"build:tests-index": "node rollup/build_test_index.cjs",
|
||||||
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||||
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; (playwright test tests/js_tests.spec.js && playwright test tests/py_tests.main.spec.js && playwright test tests/py_tests.worker.spec.js) || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test tests/js_tests.spec.js tests/py_tests.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||||
"test:ws": "bun tests/javascript/ws/index.js & playwright test tests/javascript/ws/index.spec.js",
|
"test:ws": "bun tests/javascript/ws/index.js & playwright test tests/javascript/ws/index.spec.js",
|
||||||
"dev": "node dev.cjs",
|
"dev": "node dev.cjs",
|
||||||
"release": "npm run build && npm run zip",
|
"release": "npm run build && npm run zip",
|
||||||
@@ -66,39 +62,39 @@
|
|||||||
"@webreflection/idb-map": "^0.3.2",
|
"@webreflection/idb-map": "^0.3.2",
|
||||||
"add-promise-listener": "^0.1.3",
|
"add-promise-listener": "^0.1.3",
|
||||||
"basic-devtools": "^0.1.6",
|
"basic-devtools": "^0.1.6",
|
||||||
"polyscript": "^0.16.21",
|
"polyscript": "^0.16.3",
|
||||||
"sabayon": "^0.6.6",
|
"sabayon": "^0.5.2",
|
||||||
"sticky-module": "^0.1.1",
|
"sticky-module": "^0.1.1",
|
||||||
"to-json-callback": "^0.1.1",
|
"to-json-callback": "^0.1.1",
|
||||||
"type-checked-collections": "^0.1.7"
|
"type-checked-collections": "^0.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/commands": "^6.8.0",
|
"@codemirror/commands": "^6.7.0",
|
||||||
"@codemirror/lang-python": "^6.1.7",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/language": "^6.10.8",
|
"@codemirror/language": "^6.10.3",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.4.1",
|
||||||
"@codemirror/view": "^6.36.4",
|
"@codemirror/view": "^6.34.1",
|
||||||
"@playwright/test": "^1.51.0",
|
"@playwright/test": "1.45.3",
|
||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
"@rollup/plugin-commonjs": "^28.0.1",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@webreflection/toml-j0.4": "^1.1.3",
|
"@webreflection/toml-j0.4": "^1.1.3",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"bun": "^1.1.30",
|
||||||
"bun": "^1.2.4",
|
"chokidar": "^4.0.1",
|
||||||
"chokidar": "^4.0.3",
|
|
||||||
"codedent": "^0.1.2",
|
"codedent": "^0.1.2",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"eslint": "^9.22.0",
|
"eslint": "^9.12.0",
|
||||||
"flatted": "^3.3.3",
|
"flatted": "^3.3.1",
|
||||||
"rollup": "^4.35.0",
|
"rollup": "^4.24.0",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-string": "^3.0.0",
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"static-handler": "^0.5.3",
|
"static-handler": "^0.5.3",
|
||||||
"string-width": "^7.2.0",
|
"string-width": "^7.2.0",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.6.3",
|
||||||
"xterm-readline": "^1.1.2"
|
"xterm": "^5.3.0",
|
||||||
|
"xterm-readline": "^1.1.1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const modules = {
|
|||||||
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
||||||
|
|
||||||
// xterm
|
// xterm
|
||||||
"xterm.js": resolve("@xterm/xterm"),
|
"xterm.js": resolve("xterm"),
|
||||||
"xterm-readline.js": resolve("xterm-readline"),
|
"xterm-readline.js": resolve("xterm-readline"),
|
||||||
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||||
b.text(),
|
b.text(),
|
||||||
@@ -54,9 +54,9 @@ const modules = {
|
|||||||
"xterm_addon-web-links.js": fetch(
|
"xterm_addon-web-links.js": fetch(
|
||||||
`${CDN}/@xterm/addon-web-links/+esm`,
|
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||||
).then((b) => b.text()),
|
).then((b) => b.text()),
|
||||||
"xterm.css": fetch(
|
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
||||||
`${CDN}/@xterm/xterm@${v("@xterm/xterm")}/css/xterm.min.css`,
|
(b) => b.text(),
|
||||||
).then((b) => b.text()),
|
),
|
||||||
|
|
||||||
// codemirror
|
// codemirror
|
||||||
"codemirror.js": reBundle("codemirror"),
|
"codemirror.js": reBundle("codemirror"),
|
||||||
|
|||||||
4
core/src/3rd-party/xterm-readline.js
vendored
4
core/src/3rd-party/xterm-readline.js
vendored
File diff suppressed because one or more lines are too long
4
core/src/3rd-party/xterm.css
vendored
4
core/src/3rd-party/xterm.css
vendored
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Minified by jsDelivr using clean-css v5.3.2.
|
* Minified by jsDelivr using clean-css v5.3.2.
|
||||||
* Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
|
* Original file: /npm/xterm@5.3.0/css/xterm.css
|
||||||
*
|
*
|
||||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
*/
|
*/
|
||||||
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
||||||
|
|||||||
4
core/src/3rd-party/xterm.js
vendored
4
core/src/3rd-party/xterm.js
vendored
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@ const badURL = (url, expected = "") => {
|
|||||||
* @param {string?} type the optional type to enforce
|
* @param {string?} type the optional type to enforce
|
||||||
* @returns {{json: boolean, toml: boolean, text: string}}
|
* @returns {{json: boolean, toml: boolean, text: string}}
|
||||||
*/
|
*/
|
||||||
export const configDetails = async (config, type) => {
|
const configDetails = async (config, type) => {
|
||||||
let text = config?.trim();
|
let text = config?.trim();
|
||||||
// we only support an object as root config
|
// we only support an object as root config
|
||||||
let url = "",
|
let url = "",
|
||||||
@@ -56,7 +56,7 @@ const syntaxError = (type, url, { message }) => {
|
|||||||
const configs = new Map();
|
const configs = new Map();
|
||||||
|
|
||||||
for (const [TYPE] of TYPES) {
|
for (const [TYPE] of TYPES) {
|
||||||
/** @type {() => Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
|
/** @type {Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
|
||||||
let plugins;
|
let plugins;
|
||||||
|
|
||||||
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
|
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
|
||||||
@@ -135,24 +135,24 @@ for (const [TYPE] of TYPES) {
|
|||||||
|
|
||||||
// parse all plugins and optionally ignore only
|
// parse all plugins and optionally ignore only
|
||||||
// those flagged as "undesired" via `!` prefix
|
// those flagged as "undesired" via `!` prefix
|
||||||
plugins = async () => {
|
const toBeAwaited = [];
|
||||||
const toBeAwaited = [];
|
for (const [key, value] of Object.entries(allPlugins)) {
|
||||||
for (const [key, value] of Object.entries(allPlugins)) {
|
if (error) {
|
||||||
if (error) {
|
if (key === "error") {
|
||||||
if (key === "error") {
|
// show on page the config is broken, meaning that
|
||||||
// show on page the config is broken, meaning that
|
// it was not possible to disable error plugin neither
|
||||||
// it was not possible to disable error plugin neither
|
// as that part wasn't correctly parsed anyway
|
||||||
// as that part wasn't correctly parsed anyway
|
value().then(({ notify }) => notify(error.message));
|
||||||
value().then(({ notify }) => notify(error.message));
|
|
||||||
}
|
|
||||||
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
|
||||||
toBeAwaited.push(value().then(({ default: p }) => p));
|
|
||||||
} else if (key === "error") {
|
|
||||||
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
|
|
||||||
}
|
}
|
||||||
|
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
||||||
|
toBeAwaited.push(value().then(({ default: p }) => p));
|
||||||
|
} else if (key === "error") {
|
||||||
|
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
|
||||||
}
|
}
|
||||||
return await Promise.all(toBeAwaited);
|
}
|
||||||
};
|
|
||||||
|
// assign plugins as Promise.all only if needed
|
||||||
|
plugins = Promise.all(toBeAwaited);
|
||||||
|
|
||||||
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,34 +28,48 @@ mpy-config {
|
|||||||
.py-editor-run-button,
|
.py-editor-run-button,
|
||||||
.mpy-editor-run-button {
|
.mpy-editor-run-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.25s;
|
transition: opacity 0.25s;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
.py-editor-box:hover .py-editor-run-button,
|
.py-editor-box:hover .py-editor-run-button,
|
||||||
.mpy-editor-box:hover .mpy-editor-run-button,
|
.mpy-editor-box:hover .mpy-editor-run-button,
|
||||||
.py-editor-run-button:focus,
|
.py-editor-run-button:focus,
|
||||||
.py-editor-run-button.running,
|
.py-editor-run-button:disabled,
|
||||||
.mpy-editor-run-button:focus,
|
.mpy-editor-run-button:focus,
|
||||||
.mpy-editor-run-button.running {
|
.mpy-editor-run-button:disabled {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
py-terminal span,
|
@keyframes spinner {
|
||||||
mpy-terminal span {
|
to {
|
||||||
letter-spacing: 0 !important;
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.py-editor-run-button:disabled > *,
|
||||||
dialog.pyscript-fs {
|
.mpy-editor-run-button:disabled > * {
|
||||||
border-radius: 8px;
|
display: none; /* hide all the child elements of the run button when it is disabled */
|
||||||
border-width: 1px;
|
|
||||||
}
|
}
|
||||||
|
.py-editor-run-button:disabled,
|
||||||
dialog.pyscript-fs > div {
|
.mpy-editor-run-button:disabled {
|
||||||
display: flex;
|
border-width: 0;
|
||||||
justify-content: space-between;
|
}
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,6 @@ import {
|
|||||||
createFunction,
|
createFunction,
|
||||||
inputFailure,
|
inputFailure,
|
||||||
} from "./hooks.js";
|
} from "./hooks.js";
|
||||||
import * as fs from "./fs.js";
|
|
||||||
|
|
||||||
import codemirror from "./plugins/codemirror.js";
|
|
||||||
export { codemirror };
|
|
||||||
|
|
||||||
import { stdlib, optional } from "./stdlib.js";
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
export { stdlib, optional, inputFailure };
|
export { stdlib, optional, inputFailure };
|
||||||
@@ -168,8 +164,6 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
// enrich the Python env with some JS utility for main
|
// enrich the Python env with some JS utility for main
|
||||||
interpreter.registerJsModule("_pyscript", {
|
interpreter.registerJsModule("_pyscript", {
|
||||||
PyWorker,
|
PyWorker,
|
||||||
fs,
|
|
||||||
interpreter,
|
|
||||||
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
||||||
get target() {
|
get target() {
|
||||||
return isScript(currentElement)
|
return isScript(currentElement)
|
||||||
@@ -185,7 +179,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
|||||||
// ensure plugins are bootstrapped already before custom type definition
|
// ensure plugins are bootstrapped already before custom type definition
|
||||||
// NOTE: we cannot top-level await in here as plugins import other utilities
|
// NOTE: we cannot top-level await in here as plugins import other utilities
|
||||||
// from core.js itself so that custom definition should not be blocking.
|
// from core.js itself so that custom definition should not be blocking.
|
||||||
plugins().then(() => {
|
plugins.then(() => {
|
||||||
// possible early errors sent by polyscript
|
// possible early errors sent by polyscript
|
||||||
const errors = new Map();
|
const errors = new Map();
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import IDBMap from "@webreflection/idb-map";
|
|
||||||
import { assign } from "polyscript/exports";
|
|
||||||
import { $$ } from "basic-devtools";
|
|
||||||
|
|
||||||
const stop = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ⚠️ these two constants MUST be passed as `fs`
|
|
||||||
// within the worker onBeforeRunAsync hook!
|
|
||||||
export const NAMESPACE = "@pyscript.fs";
|
|
||||||
export const ERROR = "storage permissions not granted";
|
|
||||||
|
|
||||||
export const idb = new IDBMap(NAMESPACE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ask a user action via dialog and returns the directory handler once granted.
|
|
||||||
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
|
||||||
* @returns {Promise<FileSystemDirectoryHandle>}
|
|
||||||
*/
|
|
||||||
export const getFileSystemDirectoryHandle = async (options) => {
|
|
||||||
if (!("showDirectoryPicker" in globalThis)) {
|
|
||||||
return Promise.reject(
|
|
||||||
new Error("showDirectoryPicker is not supported"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { promise, resolve, reject } = Promise.withResolvers();
|
|
||||||
|
|
||||||
const how = { id: "pyscript", mode: "readwrite", ...options };
|
|
||||||
if (options.hint) how.startIn = options.hint;
|
|
||||||
|
|
||||||
const transient = async () => {
|
|
||||||
try {
|
|
||||||
/* eslint-disable */
|
|
||||||
const handler = await showDirectoryPicker(how);
|
|
||||||
/* eslint-enable */
|
|
||||||
if ((await handler.requestPermission(how)) === "granted") {
|
|
||||||
resolve(handler);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch ({ message }) {
|
|
||||||
console.warn(message);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// in case the user decided to attach the event itself
|
|
||||||
// as opposite of relying our dialog walkthrough
|
|
||||||
if (navigator.userActivation?.isActive) {
|
|
||||||
if (!(await transient())) reject(new Error(ERROR));
|
|
||||||
} else {
|
|
||||||
const dialog = assign(document.createElement("dialog"), {
|
|
||||||
className: "pyscript-fs",
|
|
||||||
innerHTML: [
|
|
||||||
"<strong>ℹ️ Persistent FileSystem</strong><hr>",
|
|
||||||
"<p><small>PyScript would like to access a local folder.</small></p>",
|
|
||||||
"<div><button title='ok'>✅ Authorize</button>",
|
|
||||||
"<button title='cancel'>❌</button></div>",
|
|
||||||
].join(""),
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ok, cancel] = $$("button", dialog);
|
|
||||||
|
|
||||||
ok.addEventListener("click", async (event) => {
|
|
||||||
stop(event);
|
|
||||||
if (await transient()) dialog.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
cancel.addEventListener("click", async (event) => {
|
|
||||||
stop(event);
|
|
||||||
reject(new Error(ERROR));
|
|
||||||
dialog.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(dialog).showModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
};
|
|
||||||
@@ -88,19 +88,7 @@ export const hooks = {
|
|||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
onBeforeRun: new SetFunction(),
|
onBeforeRun: new SetFunction(),
|
||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
onBeforeRunAsync: new SetFunction([
|
onBeforeRunAsync: new SetFunction(),
|
||||||
({ interpreter }) => {
|
|
||||||
interpreter.registerJsModule("_pyscript", {
|
|
||||||
// cannot be imported from fs.js
|
|
||||||
// because this code is stringified
|
|
||||||
fs: {
|
|
||||||
ERROR: "storage permissions not granted",
|
|
||||||
NAMESPACE: "@pyscript.fs",
|
|
||||||
},
|
|
||||||
interpreter,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
onAfterRun: new SetFunction(),
|
onAfterRun: new SetFunction(),
|
||||||
/** @type {Set<function>} */
|
/** @type {Set<function>} */
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
// ⚠️ This file is an artifact: DO NOT MODIFY
|
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||||
export default {
|
export default {
|
||||||
codemirror: () =>
|
|
||||||
import(
|
|
||||||
/* webpackIgnore: true */
|
|
||||||
"./plugins/codemirror.js"
|
|
||||||
),
|
|
||||||
["deprecations-manager"]: () =>
|
["deprecations-manager"]: () =>
|
||||||
import(
|
import(
|
||||||
/* webpackIgnore: true */
|
/* webpackIgnore: true */
|
||||||
@@ -25,11 +20,6 @@ export default {
|
|||||||
/* webpackIgnore: true */
|
/* webpackIgnore: true */
|
||||||
"./plugins/py-editor.js"
|
"./plugins/py-editor.js"
|
||||||
),
|
),
|
||||||
["py-game"]: () =>
|
|
||||||
import(
|
|
||||||
/* webpackIgnore: true */
|
|
||||||
"./plugins/py-game.js"
|
|
||||||
),
|
|
||||||
["py-terminal"]: () =>
|
["py-terminal"]: () =>
|
||||||
import(
|
import(
|
||||||
/* webpackIgnore: true */
|
/* webpackIgnore: true */
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
// lazy loaded on-demand codemirror related files
|
|
||||||
export default {
|
|
||||||
get core() {
|
|
||||||
return import(/* webpackIgnore: true */ "../3rd-party/codemirror.js");
|
|
||||||
},
|
|
||||||
get state() {
|
|
||||||
return import(
|
|
||||||
/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
get python() {
|
|
||||||
return import(
|
|
||||||
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
get language() {
|
|
||||||
return import(
|
|
||||||
/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
get view() {
|
|
||||||
return import(
|
|
||||||
/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
get commands() {
|
|
||||||
return import(
|
|
||||||
/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// PyScript Derepcations Plugin
|
// PyScript Derepcations Plugin
|
||||||
import { notify } from "./error.js";
|
|
||||||
import { hooks } from "../core.js";
|
import { hooks } from "../core.js";
|
||||||
|
import { notify } from "./error.js";
|
||||||
|
|
||||||
// react lazily on PyScript bootstrap
|
// react lazily on PyScript bootstrap
|
||||||
hooks.main.onReady.add(checkDeprecations);
|
hooks.main.onReady.add(checkDeprecations);
|
||||||
|
|||||||
@@ -5,15 +5,8 @@ const { stringify } = JSON;
|
|||||||
|
|
||||||
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
||||||
|
|
||||||
const donkey = ({
|
const donkey = ({ type = "py", persistent, terminal, config }) => {
|
||||||
type = "py",
|
const args = persistent ? ["globals()", "__locals__"] : ["{}", "{}"];
|
||||||
persistent,
|
|
||||||
terminal,
|
|
||||||
config,
|
|
||||||
serviceWorker,
|
|
||||||
}) => {
|
|
||||||
const globals = terminal ? '{"__terminal__":__terminal__}' : "{}";
|
|
||||||
const args = persistent ? ["globals()", "__locals__"] : [globals, "{}"];
|
|
||||||
|
|
||||||
const src = URL.createObjectURL(
|
const src = URL.createObjectURL(
|
||||||
new Blob([
|
new Blob([
|
||||||
@@ -52,7 +45,6 @@ const donkey = ({
|
|||||||
typeof config === "string" ? config : stringify(config),
|
typeof config === "string" ? config : stringify(config),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (serviceWorker) script.setAttribute("service-worker", serviceWorker);
|
|
||||||
|
|
||||||
return addPromiseListener(
|
return addPromiseListener(
|
||||||
document.body.appendChild(script),
|
document.body.appendChild(script),
|
||||||
|
|||||||
@@ -2,17 +2,14 @@
|
|||||||
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||||
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
|
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
|
||||||
import { notify } from "./error.js";
|
import { notify } from "./error.js";
|
||||||
import codemirror from "./codemirror.js";
|
|
||||||
|
|
||||||
const RUN_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,12a1,1,0,0,1-.55.89l-10,5A1,1,0,0,1,8,18a1,1,0,0,1-.53-.15A1,1,0,0,1,7,17V7a1,1,0,0,1,1.45-.89l10,5A1,1,0,0,1,19,12Z" fill="#464646"/></svg>`;
|
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
||||||
const STOP_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 7h10v10H7z" style="fill:#464646;stroke:#464646;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"/></svg>`;
|
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
const getID = (type) => `${type}-editor-${id++}`;
|
const getID = (type) => `${type}-editor-${id++}`;
|
||||||
|
|
||||||
const envs = new Map();
|
const envs = new Map();
|
||||||
const configs = new Map();
|
const configs = new Map();
|
||||||
const editors = new WeakMap();
|
|
||||||
|
|
||||||
const hooks = {
|
const hooks = {
|
||||||
worker: {
|
worker: {
|
||||||
@@ -32,18 +29,12 @@ const validate = (config, result) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRelatedScript = (target, type) => {
|
|
||||||
const editor = target.closest(`.${type}-editor-box`);
|
|
||||||
return editor?.parentNode?.previousElementSibling;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function execute({ currentTarget }) {
|
async function execute({ currentTarget }) {
|
||||||
const { env, pySrc, outDiv } = this;
|
const { env, pySrc, outDiv } = this;
|
||||||
const hasRunButton = !!currentTarget;
|
const hasRunButton = !!currentTarget;
|
||||||
|
|
||||||
if (hasRunButton) {
|
if (hasRunButton) {
|
||||||
currentTarget.classList.add("running");
|
currentTarget.disabled = true;
|
||||||
currentTarget.innerHTML = STOP_BUTTON;
|
|
||||||
outDiv.innerHTML = "";
|
outDiv.innerHTML = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +81,8 @@ async function execute({ currentTarget }) {
|
|||||||
// creation and destruction of editors on the fly
|
// creation and destruction of editors on the fly
|
||||||
if (hasRunButton) {
|
if (hasRunButton) {
|
||||||
for (const type of TYPES.keys()) {
|
for (const type of TYPES.keys()) {
|
||||||
const script = getRelatedScript(currentTarget, type);
|
const editor = currentTarget.closest(`.${type}-editor-box`);
|
||||||
|
const script = editor?.parentNode?.previousElementSibling;
|
||||||
if (script) {
|
if (script) {
|
||||||
defineProperties(script, { xworker: { value: xworker } });
|
defineProperties(script, { xworker: { value: xworker } });
|
||||||
break;
|
break;
|
||||||
@@ -123,10 +115,7 @@ async function execute({ currentTarget }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const enable = () => {
|
const enable = () => {
|
||||||
if (hasRunButton) {
|
if (hasRunButton) currentTarget.disabled = false;
|
||||||
currentTarget.classList.remove("running");
|
|
||||||
currentTarget.innerHTML = RUN_BUTTON;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const { sync } = xworker;
|
const { sync } = xworker;
|
||||||
sync.write = (str) => {
|
sync.write = (str) => {
|
||||||
@@ -154,24 +143,6 @@ const makeRunButton = (handler, type) => {
|
|||||||
runButton.innerHTML = RUN_BUTTON;
|
runButton.innerHTML = RUN_BUTTON;
|
||||||
runButton.setAttribute("aria-label", "Python Script Run Button");
|
runButton.setAttribute("aria-label", "Python Script Run Button");
|
||||||
runButton.addEventListener("click", async (event) => {
|
runButton.addEventListener("click", async (event) => {
|
||||||
if (
|
|
||||||
runButton.classList.contains("running") &&
|
|
||||||
confirm("Stop evaluating this code?")
|
|
||||||
) {
|
|
||||||
const script = getRelatedScript(runButton, type);
|
|
||||||
if (script) {
|
|
||||||
const editor = editors.get(script);
|
|
||||||
const content = editor.state.doc.toString();
|
|
||||||
const clone = script.cloneNode(true);
|
|
||||||
clone.type = `${type}-editor`;
|
|
||||||
clone.textContent = content;
|
|
||||||
script.xworker.terminate();
|
|
||||||
script.nextElementSibling.remove();
|
|
||||||
script.replaceWith(clone);
|
|
||||||
editors.delete(script);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
runButton.blur();
|
runButton.blur();
|
||||||
await handler.handleEvent(event);
|
await handler.handleEvent(event);
|
||||||
});
|
});
|
||||||
@@ -223,12 +194,14 @@ const init = async (script, type, interpreter) => {
|
|||||||
{ keymap },
|
{ keymap },
|
||||||
{ defaultKeymap, indentWithTab },
|
{ defaultKeymap, indentWithTab },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
codemirror.core,
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
||||||
codemirror.state,
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
||||||
codemirror.python,
|
import(
|
||||||
codemirror.language,
|
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
||||||
codemirror.view,
|
),
|
||||||
codemirror.commands,
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"),
|
||||||
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"),
|
||||||
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let isSetup = script.hasAttribute("setup");
|
let isSetup = script.hasAttribute("setup");
|
||||||
@@ -415,7 +388,6 @@ const init = async (script, type, interpreter) => {
|
|||||||
doc,
|
doc,
|
||||||
});
|
});
|
||||||
|
|
||||||
editors.set(script, editor);
|
|
||||||
editor.focus();
|
editor.focus();
|
||||||
notifyEditor();
|
notifyEditor();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
import {
|
|
||||||
dedent,
|
|
||||||
define,
|
|
||||||
createProgress,
|
|
||||||
loadProgress,
|
|
||||||
} from "polyscript/exports";
|
|
||||||
|
|
||||||
import { stdlib } from "../core.js";
|
|
||||||
import { configDetails } from "../config.js";
|
|
||||||
import { getText } from "../fetch.js";
|
|
||||||
|
|
||||||
const progress = createProgress("py-game");
|
|
||||||
|
|
||||||
const inputPatch = `
|
|
||||||
import builtins
|
|
||||||
def input(prompt=""):
|
|
||||||
import js
|
|
||||||
return js.prompt(prompt)
|
|
||||||
|
|
||||||
builtins.input = input
|
|
||||||
del builtins
|
|
||||||
del input
|
|
||||||
`;
|
|
||||||
|
|
||||||
let toBeWarned = true;
|
|
||||||
|
|
||||||
const hooks = {
|
|
||||||
main: {
|
|
||||||
onReady: async (wrap, script) => {
|
|
||||||
if (toBeWarned) {
|
|
||||||
toBeWarned = false;
|
|
||||||
console.warn("⚠️ EXPERIMENTAL `py-game` FEATURE");
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = {};
|
|
||||||
if (script.hasAttribute("config")) {
|
|
||||||
const value = script.getAttribute("config");
|
|
||||||
const { json, toml, text, url } = await configDetails(value);
|
|
||||||
if (json) config = JSON.parse(text);
|
|
||||||
else if (toml) {
|
|
||||||
const { parse } = await import(
|
|
||||||
/* webpackIgnore: true */ "../3rd-party/toml.js"
|
|
||||||
);
|
|
||||||
config = parse(text);
|
|
||||||
}
|
|
||||||
if (config.packages) {
|
|
||||||
await wrap.interpreter.loadPackage("micropip");
|
|
||||||
const micropip = wrap.interpreter.pyimport("micropip");
|
|
||||||
await micropip.install(config.packages, {
|
|
||||||
keep_going: true,
|
|
||||||
});
|
|
||||||
micropip.destroy();
|
|
||||||
}
|
|
||||||
await loadProgress(
|
|
||||||
"py-game",
|
|
||||||
progress,
|
|
||||||
wrap.interpreter,
|
|
||||||
config,
|
|
||||||
url ? new URL(url, location.href).href : location.href,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
wrap.interpreter.registerJsModule("_pyscript", {
|
|
||||||
PyWorker() {
|
|
||||||
throw new Error(
|
|
||||||
"Unable to use PyWorker in py-game scripts",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
js_import: (...urls) =>
|
|
||||||
Promise.all(urls.map((url) => import(url))),
|
|
||||||
get target() {
|
|
||||||
return script.id;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await wrap.interpreter.runPythonAsync(stdlib);
|
|
||||||
wrap.interpreter.runPython(inputPatch);
|
|
||||||
|
|
||||||
let code = dedent(script.textContent);
|
|
||||||
if (script.src) code = await fetch(script.src).then(getText);
|
|
||||||
|
|
||||||
const target = script.getAttribute("target") || "canvas";
|
|
||||||
const canvas = document.getElementById(target);
|
|
||||||
wrap.interpreter.canvas.setCanvas2D(canvas);
|
|
||||||
|
|
||||||
// allow 3rd party to hook themselves right before
|
|
||||||
// the code gets executed
|
|
||||||
const event = new CustomEvent("py-game", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
detail: {
|
|
||||||
canvas,
|
|
||||||
code,
|
|
||||||
config,
|
|
||||||
wrap,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
script.dispatchEvent(event);
|
|
||||||
// run only if the default was not prevented
|
|
||||||
if (!event.defaultPrevented)
|
|
||||||
await wrap.interpreter.runPythonAsync(code);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
define("py-game", {
|
|
||||||
config: { packages: ["pygame-ce"] },
|
|
||||||
configURL: new URL("./config.txt", location.href).href,
|
|
||||||
interpreter: "pyodide",
|
|
||||||
env: "py-game",
|
|
||||||
hooks,
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// PyScript pyodide terminal plugin
|
// PyScript pyodide terminal plugin
|
||||||
import { defineProperties } from "polyscript/exports";
|
|
||||||
import { hooks, inputFailure } from "../../core.js";
|
import { hooks, inputFailure } from "../../core.js";
|
||||||
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
|
||||||
const bootstrapped = new WeakSet();
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
@@ -34,8 +34,6 @@ const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
|||||||
pyterminal_write(String(error.message || error));
|
pyterminal_write(String(error.message || error));
|
||||||
};
|
};
|
||||||
|
|
||||||
sync.pyterminal_stream_write = () => {};
|
|
||||||
|
|
||||||
// tiny shim of the code module with only interact
|
// tiny shim of the code module with only interact
|
||||||
// to bootstrap a REPL like environment
|
// to bootstrap a REPL like environment
|
||||||
interpreter.registerJsModule("code", {
|
interpreter.registerJsModule("code", {
|
||||||
@@ -73,7 +71,6 @@ export default async (element) => {
|
|||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: "block",
|
cursorStyle: "block",
|
||||||
lineHeight: 1.2,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream;
|
let stream;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// PyScript py-terminal plugin
|
// PyScript py-terminal plugin
|
||||||
import { defineProperties } from "polyscript/exports";
|
|
||||||
import { hooks } from "../../core.js";
|
import { hooks } from "../../core.js";
|
||||||
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
|
||||||
const bootstrapped = new WeakSet();
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
@@ -126,7 +126,6 @@ export default async (element) => {
|
|||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: "block",
|
cursorStyle: "block",
|
||||||
lineHeight: 1.2,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
xworker.sync.is_pyterminal = () => true;
|
xworker.sync.is_pyterminal = () => true;
|
||||||
@@ -137,18 +136,6 @@ export default async (element) => {
|
|||||||
// setup remote thread JS/Python code for whenever the
|
// setup remote thread JS/Python code for whenever the
|
||||||
// worker is ready to become a terminal
|
// worker is ready to become a terminal
|
||||||
hooks.worker.onReady.add(workerReady);
|
hooks.worker.onReady.add(workerReady);
|
||||||
|
|
||||||
// @see https://github.com/pyscript/pyscript/issues/2246
|
|
||||||
const patchInput = [
|
|
||||||
"import builtins as _b",
|
|
||||||
"from pyscript import sync as _s",
|
|
||||||
"_b.input = _s.pyterminal_read",
|
|
||||||
"del _b",
|
|
||||||
"del _s",
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
hooks.worker.codeBeforeRun.add(patchInput);
|
|
||||||
hooks.worker.codeBeforeRunAsync.add(patchInput);
|
|
||||||
} else {
|
} else {
|
||||||
// in the main case, just bootstrap XTerm without
|
// in the main case, just bootstrap XTerm without
|
||||||
// allowing any input as that's not possible / awkward
|
// allowing any input as that's not possible / awkward
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -30,6 +30,9 @@
|
|||||||
# as it works transparently in both the main thread and worker cases.
|
# as it works transparently in both the main thread and worker cases.
|
||||||
|
|
||||||
from polyscript import lazy_py_modules as py_import
|
from polyscript import lazy_py_modules as py_import
|
||||||
|
from pyscript.event_handling import when
|
||||||
|
from pyscript.display import HTML, display
|
||||||
|
from pyscript.fetch import fetch
|
||||||
from pyscript.magic_js import (
|
from pyscript.magic_js import (
|
||||||
RUNNING_IN_WORKER,
|
RUNNING_IN_WORKER,
|
||||||
PyWorker,
|
PyWorker,
|
||||||
@@ -41,11 +44,8 @@ from pyscript.magic_js import (
|
|||||||
sync,
|
sync,
|
||||||
window,
|
window,
|
||||||
)
|
)
|
||||||
from pyscript.display import HTML, display
|
|
||||||
from pyscript.fetch import fetch
|
|
||||||
from pyscript.storage import Storage, storage
|
from pyscript.storage import Storage, storage
|
||||||
from pyscript.websocket import WebSocket
|
from pyscript.websocket import WebSocket
|
||||||
from pyscript.events import when, Event
|
|
||||||
|
|
||||||
if not RUNNING_IN_WORKER:
|
if not RUNNING_IN_WORKER:
|
||||||
from pyscript.workers import create_named_worker, workers
|
from pyscript.workers import create_named_worker, workers
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ def _eval_formatter(obj, print_method):
|
|||||||
"""
|
"""
|
||||||
if print_method == "__repr__":
|
if print_method == "__repr__":
|
||||||
return repr(obj)
|
return repr(obj)
|
||||||
if hasattr(obj, print_method):
|
elif hasattr(obj, print_method):
|
||||||
if print_method == "savefig":
|
if print_method == "savefig":
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
obj.savefig(buf, format="png")
|
obj.savefig(buf, format="png")
|
||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
return base64.b64encode(buf.read()).decode("utf-8")
|
return base64.b64encode(buf.read()).decode("utf-8")
|
||||||
return getattr(obj, print_method)()
|
return getattr(obj, print_method)()
|
||||||
if print_method == "_repr_mimebundle_":
|
elif print_method == "_repr_mimebundle_":
|
||||||
return {}, {}
|
return {}, {}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ def _format_mime(obj):
|
|||||||
|
|
||||||
if output is None:
|
if output is None:
|
||||||
continue
|
continue
|
||||||
if mime_type not in _MIME_RENDERERS:
|
elif mime_type not in _MIME_RENDERERS:
|
||||||
not_available.append(mime_type)
|
not_available.append(mime_type)
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
@@ -149,11 +149,9 @@ def display(*values, target=None, append=True):
|
|||||||
if target is None:
|
if target is None:
|
||||||
target = current_target()
|
target = current_target()
|
||||||
elif not isinstance(target, str):
|
elif not isinstance(target, str):
|
||||||
msg = f"target must be str or None, not {target.__class__.__name__}"
|
raise TypeError(f"target must be str or None, not {target.__class__.__name__}")
|
||||||
raise TypeError(msg)
|
|
||||||
elif target == "":
|
elif target == "":
|
||||||
msg = "Cannot have an empty target"
|
raise ValueError("Cannot have an empty target")
|
||||||
raise ValueError(msg)
|
|
||||||
elif target.startswith("#"):
|
elif target.startswith("#"):
|
||||||
# note: here target is str and not None!
|
# note: here target is str and not None!
|
||||||
# align with @when behavior
|
# align with @when behavior
|
||||||
@@ -163,8 +161,9 @@ def display(*values, target=None, append=True):
|
|||||||
|
|
||||||
# If target cannot be found on the page, a ValueError is raised
|
# If target cannot be found on the page, a ValueError is raised
|
||||||
if element is None:
|
if element is None:
|
||||||
msg = f"Invalid selector with id={target}. Cannot be found in the page."
|
raise ValueError(
|
||||||
raise ValueError(msg)
|
f"Invalid selector with id={target}. Cannot be found in the page."
|
||||||
|
)
|
||||||
|
|
||||||
# if element is a <script type="py">, it has a 'target' attribute which
|
# if element is a <script type="py">, it has a 'target' attribute which
|
||||||
# points to the visual element holding the displayed values. In that case,
|
# points to the visual element holding the displayed values. In that case,
|
||||||
|
|||||||
109
core/src/stdlib/pyscript/event_handling.py
Normal file
109
core/src/stdlib/pyscript/event_handling.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import inspect
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyodide.ffi.wrappers import add_event_listener
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def add_event_listener(el, event_type, func):
|
||||||
|
el.addEventListener(event_type, func)
|
||||||
|
|
||||||
|
|
||||||
|
from pyscript.magic_js import document
|
||||||
|
|
||||||
|
|
||||||
|
def when(target, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
A decorator and function for attaching event handlers to DOM elements or
|
||||||
|
whenable objects.
|
||||||
|
|
||||||
|
When used as a decorator, the target is the object that will trigger the
|
||||||
|
event. The handler function is the decorated function. The handler function
|
||||||
|
will be called when the target is triggered.
|
||||||
|
|
||||||
|
When used as a function, the target is the object that will trigger the
|
||||||
|
event. The handler function is the next argument. The remaining arguments
|
||||||
|
and keyword arguments are passed to the target when it is triggered.
|
||||||
|
"""
|
||||||
|
# If "when" is called as a function, try to grab the handler from the
|
||||||
|
# arguments. If there's no handler, this must be a decorator based call.
|
||||||
|
handler = None
|
||||||
|
if args and callable(args[0]):
|
||||||
|
handler = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
elif callable(kwargs.get("handler")):
|
||||||
|
handler = kwargs.pop("handler")
|
||||||
|
|
||||||
|
# Does the target implement the when protocol?
|
||||||
|
whenable = hasattr(target, "__when__")
|
||||||
|
# If not when-able, the DOM selector for the target event.
|
||||||
|
if not whenable:
|
||||||
|
# The target is an event linked to a DOM selector. Extract the
|
||||||
|
# selector from the arguments or keyword arguments.
|
||||||
|
if args:
|
||||||
|
selector = args[0]
|
||||||
|
elif kwargs:
|
||||||
|
selector = kwargs.get("selector")
|
||||||
|
if not selector:
|
||||||
|
# There must be a selector if the target is not when-able.
|
||||||
|
raise ValueError("No selector provided.")
|
||||||
|
# Grab the DOM elements to which the target event will be attached.
|
||||||
|
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:
|
||||||
|
if isinstance(selector, list):
|
||||||
|
elements = selector
|
||||||
|
else:
|
||||||
|
elements = [selector]
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
try:
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
# Function doesn't receive events
|
||||||
|
if not sig.parameters:
|
||||||
|
# Function is async: must be awaited
|
||||||
|
if inspect.iscoroutinefunction(func):
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
await func()
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
func()
|
||||||
|
|
||||||
|
else:
|
||||||
|
wrapper = func
|
||||||
|
except AttributeError:
|
||||||
|
# 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" in str(e) and "positional arguments" in str(e):
|
||||||
|
return func()
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
if whenable:
|
||||||
|
target.__when__(wrapper, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
for el in elements:
|
||||||
|
add_event_listener(el, target, wrapper)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
if handler:
|
||||||
|
decorator(handler)
|
||||||
|
else:
|
||||||
|
return decorator
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import inspect
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from functools import wraps
|
|
||||||
from pyscript.magic_js import document
|
|
||||||
from pyscript.ffi import create_proxy
|
|
||||||
from pyscript.util import is_awaitable
|
|
||||||
from pyscript import config
|
|
||||||
|
|
||||||
|
|
||||||
class Event:
|
|
||||||
"""
|
|
||||||
Represents something that may happen at some point in the future.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._listeners = []
|
|
||||||
|
|
||||||
def trigger(self, result):
|
|
||||||
"""
|
|
||||||
Trigger the event with a result to pass into the handlers.
|
|
||||||
"""
|
|
||||||
for listener in self._listeners:
|
|
||||||
if is_awaitable(listener):
|
|
||||||
# Use create task to avoid making this an async function.
|
|
||||||
asyncio.create_task(listener(result))
|
|
||||||
else:
|
|
||||||
listener(result)
|
|
||||||
|
|
||||||
def add_listener(self, listener):
|
|
||||||
"""
|
|
||||||
Add a callable/awaitable to listen to when this event is triggered.
|
|
||||||
"""
|
|
||||||
if is_awaitable(listener) or callable(listener):
|
|
||||||
if listener not in self._listeners:
|
|
||||||
self._listeners.append(listener)
|
|
||||||
else:
|
|
||||||
msg = "Listener must be callable or awaitable."
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
def remove_listener(self, *args):
|
|
||||||
"""
|
|
||||||
Clear the specified handler functions in *args. If no handlers
|
|
||||||
provided, clear all handlers.
|
|
||||||
"""
|
|
||||||
if args:
|
|
||||||
for listener in args:
|
|
||||||
self._listeners.remove(listener)
|
|
||||||
else:
|
|
||||||
self._listeners = []
|
|
||||||
|
|
||||||
|
|
||||||
def when(target, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Add an event listener to the target element(s) for the specified event type.
|
|
||||||
|
|
||||||
The target can be a string representing the event type, or an Event object.
|
|
||||||
If the target is an Event object, the event listener will be added to that
|
|
||||||
object. If the target is a string, the event listener will be added to the
|
|
||||||
element(s) that match the (second) selector argument.
|
|
||||||
|
|
||||||
If a (third) handler argument is provided, it will be called when the event
|
|
||||||
is triggered; thus allowing this to be used as both a function and a
|
|
||||||
decorator.
|
|
||||||
"""
|
|
||||||
# If "when" is called as a function, try to grab the handler from the
|
|
||||||
# arguments. If there's no handler, this must be a decorator based call.
|
|
||||||
handler = None
|
|
||||||
if args and (callable(args[0]) or is_awaitable(args[0])):
|
|
||||||
handler = args[0]
|
|
||||||
elif callable(kwargs.get("handler")) or is_awaitable(kwargs.get("handler")):
|
|
||||||
handler = kwargs.pop("handler")
|
|
||||||
# If the target is a string, it is the "older" use of `when` where it
|
|
||||||
# represents the name of a DOM event.
|
|
||||||
if isinstance(target, str):
|
|
||||||
# Extract the selector from the arguments or keyword arguments.
|
|
||||||
selector = args[0] if args else kwargs.pop("selector")
|
|
||||||
if not selector:
|
|
||||||
msg = "No selector provided."
|
|
||||||
raise ValueError(msg)
|
|
||||||
# Grab the DOM elements to which the target event will be attached.
|
|
||||||
from pyscript.web import Element, ElementCollection
|
|
||||||
|
|
||||||
if isinstance(selector, str):
|
|
||||||
elements = document.querySelectorAll(selector)
|
|
||||||
elif isinstance(selector, Element):
|
|
||||||
elements = [selector._dom_element]
|
|
||||||
elif isinstance(selector, ElementCollection):
|
|
||||||
elements = [el._dom_element for el in selector]
|
|
||||||
else:
|
|
||||||
elements = selector if isinstance(selector, list) else [selector]
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
if config["type"] == "mpy": # Is MicroPython?
|
|
||||||
if is_awaitable(func):
|
|
||||||
|
|
||||||
async def wrapper(*args, **kwargs):
|
|
||||||
"""
|
|
||||||
This is a very ugly hack to get micropython working because
|
|
||||||
`inspect.signature` doesn't exist. It may be actually better
|
|
||||||
to not try any magic for now and raise the error.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
|
|
||||||
except TypeError as e:
|
|
||||||
if "takes" in str(e) and "positional arguments" in str(e):
|
|
||||||
return await func()
|
|
||||||
raise
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
"""
|
|
||||||
This is a very ugly hack to get micropython working because
|
|
||||||
`inspect.signature` doesn't exist. It may be actually better
|
|
||||||
to not try any magic for now and raise the error.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
except TypeError as e:
|
|
||||||
if "takes" in str(e) and "positional arguments" in str(e):
|
|
||||||
return func()
|
|
||||||
raise
|
|
||||||
|
|
||||||
else:
|
|
||||||
sig = inspect.signature(func)
|
|
||||||
if sig.parameters:
|
|
||||||
if is_awaitable(func):
|
|
||||||
|
|
||||||
async def wrapper(event):
|
|
||||||
return await func(event)
|
|
||||||
|
|
||||||
else:
|
|
||||||
wrapper = func
|
|
||||||
else:
|
|
||||||
# Function doesn't receive events.
|
|
||||||
if is_awaitable(func):
|
|
||||||
|
|
||||||
async def wrapper(*args, **kwargs):
|
|
||||||
return await func()
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return func()
|
|
||||||
|
|
||||||
wrapper = wraps(func)(wrapper)
|
|
||||||
if isinstance(target, Event):
|
|
||||||
# The target is a single Event object.
|
|
||||||
target.add_listener(wrapper)
|
|
||||||
elif isinstance(target, list) and all(isinstance(t, Event) for t in target):
|
|
||||||
# The target is a list of Event objects.
|
|
||||||
for evt in target:
|
|
||||||
evt.add_listener(wrapper)
|
|
||||||
else:
|
|
||||||
# The target is a string representing an event type, and so a
|
|
||||||
# DOM element or collection of elements is found in "elements".
|
|
||||||
for el in elements:
|
|
||||||
el.addEventListener(target, create_proxy(wrapper))
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
# If "when" was called as a decorator, return the decorator function,
|
|
||||||
# otherwise just call the internal decorator function with the supplied
|
|
||||||
# handler.
|
|
||||||
return decorator(handler) if handler else decorator
|
|
||||||
@@ -31,7 +31,7 @@ def _object_keys(value):
|
|||||||
|
|
||||||
|
|
||||||
def _is_array(value):
|
def _is_array(value):
|
||||||
return isinstance(value, (list, tuple))
|
return isinstance(value, list) or isinstance(value, tuple)
|
||||||
|
|
||||||
|
|
||||||
def _is_object(value):
|
def _is_object(value):
|
||||||
@@ -60,10 +60,10 @@ def _loop(keys, input, known, output):
|
|||||||
|
|
||||||
|
|
||||||
def _ref(key, value, input, known, output):
|
def _ref(key, value, input, known, output):
|
||||||
if _is_array(value) and value not in known:
|
if _is_array(value) and not value in known:
|
||||||
known.append(value)
|
known.append(value)
|
||||||
value = _loop(_array_keys(value), input, known, value)
|
value = _loop(_array_keys(value), input, known, value)
|
||||||
elif _is_object(value) and value not in known:
|
elif _is_object(value) and not value in known:
|
||||||
known.append(value)
|
known.append(value)
|
||||||
value = _loop(_object_keys(value), input, known, value)
|
value = _loop(_object_keys(value), input, known, value)
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
mounted = {}
|
|
||||||
|
|
||||||
|
|
||||||
async def mount(path, mode="readwrite", root="", id="pyscript"):
|
|
||||||
import js
|
|
||||||
from _pyscript import fs, interpreter
|
|
||||||
from pyscript.ffi import to_js
|
|
||||||
from pyscript.magic_js import (
|
|
||||||
RUNNING_IN_WORKER,
|
|
||||||
sync,
|
|
||||||
)
|
|
||||||
|
|
||||||
js.console.warn("experimental pyscript.fs ⚠️")
|
|
||||||
|
|
||||||
handler = None
|
|
||||||
|
|
||||||
uid = f"{path}@{id}"
|
|
||||||
|
|
||||||
options = {"id": id, "mode": mode}
|
|
||||||
if root != "":
|
|
||||||
options["startIn"] = root
|
|
||||||
|
|
||||||
if RUNNING_IN_WORKER:
|
|
||||||
fsh = sync.storeFSHandler(uid, to_js(options))
|
|
||||||
|
|
||||||
# allow both async and/or SharedArrayBuffer use case
|
|
||||||
if isinstance(fsh, bool):
|
|
||||||
success = fsh
|
|
||||||
else:
|
|
||||||
success = await fsh
|
|
||||||
|
|
||||||
if success:
|
|
||||||
from polyscript import IDBMap
|
|
||||||
|
|
||||||
idb = IDBMap.new(fs.NAMESPACE)
|
|
||||||
handler = await idb.get(uid)
|
|
||||||
else:
|
|
||||||
raise RuntimeError(fs.ERROR)
|
|
||||||
|
|
||||||
else:
|
|
||||||
success = await fs.idb.has(uid)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
handler = await fs.idb.get(uid)
|
|
||||||
else:
|
|
||||||
handler = await fs.getFileSystemDirectoryHandle(to_js(options))
|
|
||||||
await fs.idb.set(uid, handler)
|
|
||||||
|
|
||||||
mounted[path] = await interpreter.mountNativeFS(path, handler)
|
|
||||||
|
|
||||||
|
|
||||||
async def sync(path):
|
|
||||||
await mounted[path].syncfs()
|
|
||||||
|
|
||||||
|
|
||||||
async def unmount(path):
|
|
||||||
from _pyscript import interpreter
|
|
||||||
|
|
||||||
await sync(path)
|
|
||||||
interpreter._module.FS.unmount(path)
|
|
||||||
@@ -25,7 +25,6 @@ class JSModule:
|
|||||||
# avoid pyodide looking for non existent fields
|
# avoid pyodide looking for non existent fields
|
||||||
if not field.startswith("_"):
|
if not field.startswith("_"):
|
||||||
return getattr(getattr(js_modules, self.name), field)
|
return getattr(getattr(js_modules, self.name), field)
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# generate N modules in the system that will proxy the real value
|
# generate N modules in the system that will proxy the real value
|
||||||
|
|||||||
@@ -31,22 +31,26 @@ class Device:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def load(cls, audio=False, video=True):
|
async def load(cls, audio=False, video=True):
|
||||||
"""
|
"""Load the device stream."""
|
||||||
Load the device stream.
|
options = window.Object.new()
|
||||||
"""
|
options.audio = audio
|
||||||
options = {}
|
|
||||||
options["audio"] = audio
|
|
||||||
if isinstance(video, bool):
|
if isinstance(video, bool):
|
||||||
options["video"] = video
|
options.video = video
|
||||||
else:
|
else:
|
||||||
options["video"] = {}
|
# 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:
|
for k in video:
|
||||||
options["video"][k] = video[k]
|
setattr(options.video, k, to_js(video[k]))
|
||||||
return await window.navigator.mediaDevices.getUserMedia(to_js(options))
|
|
||||||
|
stream = await window.navigator.mediaDevices.getUserMedia(options)
|
||||||
|
return stream
|
||||||
|
|
||||||
async def get_stream(self):
|
async def get_stream(self):
|
||||||
key = self.kind.replace("input", "").replace("output", "")
|
key = self.kind.replace("input", "").replace("output", "")
|
||||||
options = {key: {"deviceId": {"exact": self.id}}}
|
options = {key: {"deviceId": {"exact": self.id}}}
|
||||||
|
|
||||||
return await self.load(**options)
|
return await self.load(**options)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ def _to_idb(value):
|
|||||||
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
||||||
return _stringify(["generic", value])
|
return _stringify(["generic", value])
|
||||||
if isinstance(value, bytearray):
|
if isinstance(value, bytearray):
|
||||||
return _stringify(["bytearray", list(value)])
|
return _stringify(["bytearray", [v for v in value]])
|
||||||
if isinstance(value, memoryview):
|
if isinstance(value, memoryview):
|
||||||
return _stringify(["memoryview", list(value)])
|
return _stringify(["memoryview", [v for v in value]])
|
||||||
msg = f"Unexpected value: {value}"
|
raise TypeError(f"Unexpected value: {value}")
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
# convert an IndexedDB compatible entry into a Python value
|
# convert an IndexedDB compatible entry into a Python value
|
||||||
@@ -57,6 +56,5 @@ class Storage(dict):
|
|||||||
|
|
||||||
async def storage(name="", storage_class=Storage):
|
async def storage(name="", storage_class=Storage):
|
||||||
if not name:
|
if not name:
|
||||||
msg = "The storage name must be defined"
|
raise ValueError("The storage name must be defined")
|
||||||
raise ValueError(msg)
|
|
||||||
return storage_class(await _storage(f"@pyscript/{name}"))
|
return storage_class(await _storage(f"@pyscript/{name}"))
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import js
|
import js
|
||||||
import sys
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
|
|
||||||
def as_bytearray(buffer):
|
def as_bytearray(buffer):
|
||||||
"""
|
|
||||||
Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
|
|
||||||
MicroPython friendly manner.
|
|
||||||
"""
|
|
||||||
ui8a = js.Uint8Array.new(buffer)
|
ui8a = js.Uint8Array.new(buffer)
|
||||||
size = ui8a.length
|
size = ui8a.length
|
||||||
ba = bytearray(size)
|
ba = bytearray(size)
|
||||||
for i in range(size):
|
for i in range(0, size):
|
||||||
ba[i] = ui8a[i]
|
ba[i] = ui8a[i]
|
||||||
return ba
|
return ba
|
||||||
|
|
||||||
@@ -37,22 +31,3 @@ class NotSupported:
|
|||||||
|
|
||||||
def __call__(self, *args):
|
def __call__(self, *args):
|
||||||
raise TypeError(self.error)
|
raise TypeError(self.error)
|
||||||
|
|
||||||
|
|
||||||
def is_awaitable(obj):
|
|
||||||
"""
|
|
||||||
Returns a boolean indication if the passed in obj is an awaitable
|
|
||||||
function. (MicroPython treats awaitables as generator functions, and if
|
|
||||||
the object is a closure containing an async function we need to work
|
|
||||||
carefully.)
|
|
||||||
"""
|
|
||||||
from pyscript import config
|
|
||||||
|
|
||||||
if config["type"] == "mpy": # Is MicroPython?
|
|
||||||
# MicroPython doesn't appear to have a way to determine if a closure is
|
|
||||||
# an async function except via the repr. This is a bit hacky.
|
|
||||||
if "<closure <generator>" in repr(obj):
|
|
||||||
return True
|
|
||||||
return inspect.isgeneratorfunction(obj)
|
|
||||||
|
|
||||||
return inspect.iscoroutinefunction(obj)
|
|
||||||
|
|||||||
@@ -2,11 +2,7 @@
|
|||||||
|
|
||||||
# `when` is not used in this module. It is imported here save the user an additional
|
# `when` is not used in this module. It is imported here save the user an additional
|
||||||
# import (i.e. they can get what they need from `pyscript.web`).
|
# import (i.e. they can get what they need from `pyscript.web`).
|
||||||
|
from pyscript import document, when # NOQA
|
||||||
# from __future__ import annotations # CAUTION: This is not supported in MicroPython.
|
|
||||||
|
|
||||||
from pyscript import document, when, Event # noqa: F401
|
|
||||||
from pyscript.ffi import create_proxy
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_dom_element(dom_element):
|
def wrap_dom_element(dom_element):
|
||||||
@@ -72,18 +68,6 @@ class Element:
|
|||||||
type(self).get_tag_name()
|
type(self).get_tag_name()
|
||||||
)
|
)
|
||||||
|
|
||||||
# HTML on_events attached to the element become pyscript.Event instances.
|
|
||||||
self._on_events = {}
|
|
||||||
|
|
||||||
# Handle kwargs for handling named events with a default handler function.
|
|
||||||
properties = {}
|
|
||||||
for name, handler in kwargs.items():
|
|
||||||
if name.startswith("on_"):
|
|
||||||
ev = self.get_event(name) # Create the default Event instance.
|
|
||||||
ev.add_listener(handler)
|
|
||||||
else:
|
|
||||||
properties[name] = handler
|
|
||||||
|
|
||||||
# A set-like interface to the element's `classList`.
|
# A set-like interface to the element's `classList`.
|
||||||
self._classes = Classes(self)
|
self._classes = Classes(self)
|
||||||
|
|
||||||
@@ -91,7 +75,7 @@ class Element:
|
|||||||
self._style = Style(self)
|
self._style = Style(self)
|
||||||
|
|
||||||
# Set any specified classes, styles, and DOM properties.
|
# Set any specified classes, styles, and DOM properties.
|
||||||
self.update(classes=classes, style=style, **properties)
|
self.update(classes=classes, style=style, **kwargs)
|
||||||
|
|
||||||
def __eq__(self, obj):
|
def __eq__(self, obj):
|
||||||
"""Check for equality by comparing the underlying DOM element."""
|
"""Check for equality by comparing the underlying DOM element."""
|
||||||
@@ -103,27 +87,19 @@ class Element:
|
|||||||
If `key` is an integer or a slice we use it to index/slice the element's
|
If `key` is an integer or a slice we use it to index/slice the element's
|
||||||
children. Otherwise, we use `key` as a query selector.
|
children. Otherwise, we use `key` as a query selector.
|
||||||
"""
|
"""
|
||||||
if isinstance(key, (int, slice)):
|
if isinstance(key, int) or isinstance(key, slice):
|
||||||
return self.children[key]
|
return self.children[key]
|
||||||
|
|
||||||
return self.find(key)
|
return self.find(key)
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
"""
|
|
||||||
Get an attribute from the element.
|
|
||||||
|
|
||||||
If the attribute is an event (e.g. "on_click"), we wrap it in an `Event`
|
|
||||||
instance and return that. Otherwise, we return the attribute from the
|
|
||||||
underlying DOM element.
|
|
||||||
"""
|
|
||||||
if name.startswith("on_"):
|
|
||||||
return self.get_event(name)
|
|
||||||
# This allows us to get attributes on the underlying DOM element that clash
|
# This allows us to get attributes on the underlying DOM element that clash
|
||||||
# with Python keywords or built-ins (e.g. the output element has an
|
# with Python keywords or built-ins (e.g. the output element has an
|
||||||
# attribute `for` which is a Python keyword, so you can access it on the
|
# attribute `for` which is a Python keyword, so you can access it on the
|
||||||
# Element instance via `for_`).
|
# Element instance via `for_`).
|
||||||
if name.endswith("_"):
|
if name.endswith("_"):
|
||||||
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
|
name = name[:-1]
|
||||||
|
|
||||||
return getattr(self._dom_element, name)
|
return getattr(self._dom_element, name)
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
@@ -141,37 +117,10 @@ class Element:
|
|||||||
# attribute `for` which is a Python keyword, so you can access it on the
|
# attribute `for` which is a Python keyword, so you can access it on the
|
||||||
# Element instance via `for_`).
|
# Element instance via `for_`).
|
||||||
if name.endswith("_"):
|
if name.endswith("_"):
|
||||||
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
|
name = name[:-1]
|
||||||
|
|
||||||
if name.startswith("on_"):
|
|
||||||
# Ensure on-events are cached in the _on_events dict if the
|
|
||||||
# user is setting them directly.
|
|
||||||
self._on_events[name] = value
|
|
||||||
|
|
||||||
setattr(self._dom_element, name, value)
|
setattr(self._dom_element, name, value)
|
||||||
|
|
||||||
def get_event(self, name):
|
|
||||||
"""
|
|
||||||
Get an `Event` instance for the specified event name.
|
|
||||||
"""
|
|
||||||
if not name.startswith("on_"):
|
|
||||||
msg = "Event names must start with 'on_'."
|
|
||||||
raise ValueError(msg)
|
|
||||||
event_name = name[3:] # Remove the "on_" prefix.
|
|
||||||
if not hasattr(self._dom_element, event_name):
|
|
||||||
msg = f"Element has no '{event_name}' event."
|
|
||||||
raise ValueError(msg)
|
|
||||||
if name in self._on_events:
|
|
||||||
return self._on_events[name]
|
|
||||||
# Such an on-event exists in the DOM element, but we haven't yet
|
|
||||||
# wrapped it in an Event instance. Let's do that now. When the
|
|
||||||
# underlying DOM element's event is triggered, the Event instance
|
|
||||||
# will be triggered too.
|
|
||||||
ev = Event()
|
|
||||||
self._on_events[name] = ev
|
|
||||||
self._dom_element.addEventListener(event_name, create_proxy(ev.trigger))
|
|
||||||
return ev
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children(self):
|
def children(self):
|
||||||
"""Return the element's children as an `ElementCollection`."""
|
"""Return the element's children as an `ElementCollection`."""
|
||||||
@@ -208,7 +157,7 @@ class Element:
|
|||||||
# We check for list/tuple here and NOT for any iterable as it will match
|
# We check for list/tuple here and NOT for any iterable as it will match
|
||||||
# a JS Nodelist which is handled explicitly below.
|
# a JS Nodelist which is handled explicitly below.
|
||||||
# NodeList.
|
# NodeList.
|
||||||
elif isinstance(item, (list, tuple)):
|
elif isinstance(item, list) or isinstance(item, tuple):
|
||||||
for child in item:
|
for child in item:
|
||||||
self.append(child)
|
self.append(child)
|
||||||
|
|
||||||
@@ -232,11 +181,10 @@ class Element:
|
|||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Nope! This is not an element or a NodeList.
|
# Nope! This is not an element or a NodeList.
|
||||||
msg = (
|
raise TypeError(
|
||||||
f'Element "{item}" is a proxy object, "'
|
f'Element "{item}" is a proxy object, "'
|
||||||
f"but not a valid element or a NodeList."
|
f"but not a valid element or a NodeList."
|
||||||
)
|
)
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
def clone(self, clone_id=None):
|
def clone(self, clone_id=None):
|
||||||
"""Make a clone of the element (clones the underlying DOM object too)."""
|
"""Make a clone of the element (clones the underlying DOM object too)."""
|
||||||
@@ -407,8 +355,9 @@ class Options:
|
|||||||
|
|
||||||
new_option = option(**kwargs)
|
new_option = option(**kwargs)
|
||||||
|
|
||||||
if before and isinstance(before, Element):
|
if before:
|
||||||
before = before._dom_element
|
if isinstance(before, Element):
|
||||||
|
before = before._dom_element
|
||||||
|
|
||||||
self._element._dom_element.add(new_option._dom_element, before)
|
self._element._dom_element.add(new_option._dom_element, before)
|
||||||
|
|
||||||
@@ -468,7 +417,7 @@ class ContainerElement(Element):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for child in list(args) + (children or []):
|
for child in list(args) + (children or []):
|
||||||
if isinstance(child, (Element, ElementCollection)):
|
if isinstance(child, Element) or isinstance(child, ElementCollection):
|
||||||
self.append(child)
|
self.append(child)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -498,13 +447,14 @@ class ClassesCollection:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
yield from self._all_class_names()
|
for class_name in self._all_class_names():
|
||||||
|
yield class_name
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self._all_class_names())
|
return len(self._all_class_names())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"ClassesCollection({self._collection!r})"
|
return f"ClassesCollection({repr(self._collection)})"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return " ".join(self._all_class_names())
|
return " ".join(self._all_class_names())
|
||||||
@@ -557,7 +507,7 @@ class StyleCollection:
|
|||||||
element.style[key] = value
|
element.style[key] = value
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"StyleCollection({self._collection!r})"
|
return f"StyleCollection({repr(self._collection)})"
|
||||||
|
|
||||||
def remove(self, key):
|
def remove(self, key):
|
||||||
"""Remove a CSS property from the elements in the collection."""
|
"""Remove a CSS property from the elements in the collection."""
|
||||||
@@ -592,7 +542,7 @@ class ElementCollection:
|
|||||||
if isinstance(key, int):
|
if isinstance(key, int):
|
||||||
return self._elements[key]
|
return self._elements[key]
|
||||||
|
|
||||||
if isinstance(key, slice):
|
elif isinstance(key, slice):
|
||||||
return ElementCollection(self._elements[key])
|
return ElementCollection(self._elements[key])
|
||||||
|
|
||||||
return self.find(key)
|
return self.find(key)
|
||||||
@@ -1129,8 +1079,7 @@ class video(ContainerElement):
|
|||||||
|
|
||||||
elif isinstance(to, Element):
|
elif isinstance(to, Element):
|
||||||
if to.tag != "canvas":
|
if to.tag != "canvas":
|
||||||
msg = "Element to snap to must be a canvas."
|
raise TypeError("Element to snap to must be a canvas.")
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
elif getattr(to, "tagName", "") == "CANVAS":
|
elif getattr(to, "tagName", "") == "CANVAS":
|
||||||
to = canvas(dom_element=to)
|
to = canvas(dom_element=to)
|
||||||
@@ -1139,12 +1088,10 @@ class video(ContainerElement):
|
|||||||
elif isinstance(to, str):
|
elif isinstance(to, str):
|
||||||
nodelist = document.querySelectorAll(to) # NOQA
|
nodelist = document.querySelectorAll(to) # NOQA
|
||||||
if nodelist.length == 0:
|
if nodelist.length == 0:
|
||||||
msg = "No element with selector {to} to snap to."
|
raise TypeError("No element with selector {to} to snap to.")
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
if nodelist[0].tagName != "CANVAS":
|
if nodelist[0].tagName != "CANVAS":
|
||||||
msg = "Element to snap to must be a canvas."
|
raise TypeError("Element to snap to must be a canvas.")
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
to = canvas(dom_element=nodelist[0])
|
to = canvas(dom_element=nodelist[0])
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class EventMessage:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class WebSocket:
|
class WebSocket(object):
|
||||||
CONNECTING = 0
|
CONNECTING = 0
|
||||||
OPEN = 1
|
OPEN = 1
|
||||||
CLOSING = 2
|
CLOSING = 2
|
||||||
|
|||||||
@@ -25,12 +25,10 @@ async def create_named_worker(src="", name="", config=None, type="py"):
|
|||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
if not src:
|
if not src:
|
||||||
msg = "Named workers require src"
|
raise ValueError("Named workers require src")
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
msg = "Named workers require a name"
|
raise ValueError("Named workers require a name")
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
s = _js.document.createElement("script")
|
s = _js.document.createElement("script")
|
||||||
s.type = type
|
s.type = type
|
||||||
@@ -39,7 +37,7 @@ async def create_named_worker(src="", name="", config=None, type="py"):
|
|||||||
_set(s, "name", name)
|
_set(s, "name", name)
|
||||||
|
|
||||||
if config:
|
if config:
|
||||||
_set(s, "config", (isinstance(config, str) and config) or dumps(config))
|
_set(s, "config", isinstance(config, str) and config or dumps(config))
|
||||||
|
|
||||||
_js.document.body.append(s)
|
_js.document.body.append(s)
|
||||||
return await workers[name]
|
return await workers[name]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { idb, getFileSystemDirectoryHandle } from "./fs.js";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// allow pyterminal checks to bootstrap
|
// allow pyterminal checks to bootstrap
|
||||||
is_pyterminal: () => false,
|
is_pyterminal: () => false,
|
||||||
@@ -11,21 +9,4 @@ export default {
|
|||||||
sleep(seconds) {
|
sleep(seconds) {
|
||||||
return new Promise(($) => setTimeout($, seconds * 1000));
|
return new Promise(($) => setTimeout($, seconds * 1000));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Ask a user action via dialog and returns the directory handler once granted.
|
|
||||||
* @param {string} uid
|
|
||||||
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
async storeFSHandler(uid, options = {}) {
|
|
||||||
if (await idb.has(uid)) return true;
|
|
||||||
return getFileSystemDirectoryHandle(options).then(
|
|
||||||
async (handler) => {
|
|
||||||
await idb.set(uid, handler);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
() => false,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,39 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Pyodide Media Module Test</title>
|
|
||||||
<link rel="stylesheet" href="../../dist/core.css">
|
|
||||||
<script type="module" src="../../dist/core.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Pyodide Media Module Test</h1>
|
|
||||||
<div id="test-results">Running tests...</div>
|
|
||||||
|
|
||||||
<script type="py" terminal>
|
|
||||||
from pyscript import window, document
|
|
||||||
from pyscript import media
|
|
||||||
|
|
||||||
async def run_tests():
|
|
||||||
# Test basic module structure
|
|
||||||
assert hasattr(media, "Device"), "media module should have Device class"
|
|
||||||
assert hasattr(media, "list_devices"), "media module should have list_devices function"
|
|
||||||
|
|
||||||
# Test device enumeration
|
|
||||||
devices = await media.list_devices()
|
|
||||||
assert isinstance(devices, list), "list_devices should return a list"
|
|
||||||
|
|
||||||
# If we have devices, test properties of one
|
|
||||||
if devices:
|
|
||||||
device = devices[0]
|
|
||||||
assert hasattr(device, "id"), "Device should have id property"
|
|
||||||
assert hasattr(device, "group"), "Device should have group property"
|
|
||||||
assert hasattr(device, "kind"), "Device should have kind property"
|
|
||||||
assert hasattr(device, "label"), "Device should have label property"
|
|
||||||
|
|
||||||
document.getElementById('test-results').innerText = "Success!"
|
|
||||||
document.documentElement.classList.add('media-ok')
|
|
||||||
|
|
||||||
await run_tests()
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import numpy as np
|
import numpy
|
||||||
import matplotlib as mpl
|
import matplotlib
|
||||||
|
|
||||||
# just do something with the packages
|
# just do something with the packages
|
||||||
print(len(dir(np)))
|
print(len(dir(numpy)))
|
||||||
print(len(dir(mpl)))
|
print(len(dir(matplotlib)))
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<!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" config='{"packages":["jsonpointer==3.0.0"]}'>
|
|
||||||
import jsonpointer
|
|
||||||
from pyscript import document
|
|
||||||
document.documentElement.classList.add("done")
|
|
||||||
document.body.append("OK")
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -4,4 +4,4 @@ def runtime_version():
|
|||||||
return sys.version
|
return sys.version
|
||||||
|
|
||||||
|
|
||||||
__export__ = ["runtime_version"]
|
__export__ = ['runtime_version']
|
||||||
|
|||||||
@@ -139,25 +139,6 @@ test('Pyodide lockFileURL vs CDN', async ({ page }) => {
|
|||||||
await expect(body).toBe('OK');
|
await expect(body).toBe('OK');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Pyodide pinned lockFileURL', async ({ page }) => {
|
|
||||||
const logs = [];
|
|
||||||
page.on('console', msg => {
|
|
||||||
const text = msg.text();
|
|
||||||
if (!text.startsWith('['))
|
|
||||||
logs.push(text);
|
|
||||||
});
|
|
||||||
await page.goto('http://localhost:8080/tests/javascript/pyodide-lockfile/');
|
|
||||||
await page.waitForSelector('html.done');
|
|
||||||
let body = await page.evaluate(() => document.body.lastChild.textContent);
|
|
||||||
await expect(body).toBe('OK');
|
|
||||||
await expect(!!logs.splice(0).length).toBe(true);
|
|
||||||
await page.reload();
|
|
||||||
await page.waitForSelector('html.done');
|
|
||||||
body = await page.evaluate(() => document.body.lastChild.textContent);
|
|
||||||
await expect(body).toBe('OK');
|
|
||||||
await expect(logs.splice(0).length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('MicroPython buffered error', async ({ page }) => {
|
test('MicroPython buffered error', async ({ page }) => {
|
||||||
await page.goto('http://localhost:8080/tests/javascript/mpy-error.html');
|
await page.goto('http://localhost:8080/tests/javascript/mpy-error.html');
|
||||||
await page.waitForSelector('html.ok');
|
await page.waitForSelector('html.ok');
|
||||||
@@ -171,24 +152,3 @@ test('MicroPython buffered NO error', async ({ page }) => {
|
|||||||
const body = await page.evaluate(() => document.body.textContent.trim());
|
const body = await page.evaluate(() => document.body.textContent.trim());
|
||||||
await expect(body).toBe('');
|
await expect(body).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Pyodide media module', async ({ page }) => {
|
|
||||||
await page.context().grantPermissions(['camera', 'microphone']);
|
|
||||||
await page.context().addInitScript(() => {
|
|
||||||
const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices;
|
|
||||||
navigator.mediaDevices.enumerateDevices = async function() {
|
|
||||||
const realDevices = await originalEnumerateDevices.call(this);
|
|
||||||
if (!realDevices || realDevices.length === 0) {
|
|
||||||
return [
|
|
||||||
{ deviceId: 'camera1', groupId: 'group1', kind: 'videoinput', label: 'Simulated Camera' },
|
|
||||||
{ deviceId: 'mic1', groupId: 'group2', kind: 'audioinput', label: 'Simulated Microphone' }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return realDevices;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
await page.goto('http://localhost:8080/tests/javascript/media.html');
|
|
||||||
await page.waitForSelector('html.media-ok', { timeout: 10000 });
|
|
||||||
const isSuccess = await page.evaluate(() => document.documentElement.classList.contains('media-ok'));
|
|
||||||
expect(isSuccess).toBe(true);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<!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" src="emoji.py" terminal worker></script>
|
|
||||||
<script type="py" src="emoji.py" terminal worker></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
print(sys.version)
|
|
||||||
RED = chr(0x1F534) # LARGE RED CIRCLE
|
|
||||||
GREEN = chr(0x1F7E2) # LARGE GREEN CIRCLE
|
|
||||||
MOUSE = chr(0x1F42D) # MOUSE FACE
|
|
||||||
EARTH = chr(0x1F30E) # EARTH GLOBE AMERICAS
|
|
||||||
FACE = chr(0x1F610) # NEUTRAL FACE
|
|
||||||
BASMALA = chr(0xFDFD) # ARABIC LIGATURE BISMILLAH AR-RAHMAN AR-RAHEEM
|
|
||||||
|
|
||||||
print("[", RED, "]")
|
|
||||||
print("[", MOUSE, "]")
|
|
||||||
print("[", EARTH, "]")
|
|
||||||
print("[", FACE, "]")
|
|
||||||
print("[", FACE * 3, "]")
|
|
||||||
print("[", BASMALA, "]")
|
|
||||||
print("[", BASMALA + GREEN, "]")
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<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" src="index.py"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import os
|
|
||||||
from pyscript import RUNNING_IN_WORKER, fs
|
|
||||||
|
|
||||||
|
|
||||||
TEST = "implicit"
|
|
||||||
|
|
||||||
if TEST == "implicit":
|
|
||||||
await fs.mount("/persistent")
|
|
||||||
|
|
||||||
print(
|
|
||||||
(RUNNING_IN_WORKER and "Worker") or "Main",
|
|
||||||
os.listdir("/persistent"),
|
|
||||||
)
|
|
||||||
|
|
||||||
from random import random
|
|
||||||
|
|
||||||
with open("/persistent/random.txt", "w") as f:
|
|
||||||
f.write(str(random()))
|
|
||||||
|
|
||||||
await fs.sync("/persistent")
|
|
||||||
|
|
||||||
elif not RUNNING_IN_WORKER:
|
|
||||||
from pyscript import document
|
|
||||||
|
|
||||||
button = document.createElement("button")
|
|
||||||
button.textContent = "mount"
|
|
||||||
document.body.append(button)
|
|
||||||
|
|
||||||
async def mount(event):
|
|
||||||
try:
|
|
||||||
await fs.mount("/persistent")
|
|
||||||
print(os.listdir("/persistent"))
|
|
||||||
button.textContent = "unmount"
|
|
||||||
button.onclick = unmount
|
|
||||||
|
|
||||||
except:
|
|
||||||
import js
|
|
||||||
|
|
||||||
js.alert("unable to grant access")
|
|
||||||
|
|
||||||
async def unmount(event):
|
|
||||||
await fs.unmount("/persistent")
|
|
||||||
button.textContent = "mount"
|
|
||||||
button.onclick = mount
|
|
||||||
|
|
||||||
button.onclick = mount
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/* (c) https://github.com/ryanking13/pyodide-pygame-demo/blob/main/examples/aliens.html */
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.demo {
|
|
||||||
background-color: #fff;
|
|
||||||
margin: 20px auto;
|
|
||||||
max-width: 1000px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.demo-header {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
padding: 15px 20px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
.demo-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#canvas {
|
|
||||||
margin: 0 auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
"""(c) https://github.com/ryanking13/pyodide-pygame-demo/blob/main/examples/aliens.html
|
|
||||||
pygame.examples.aliens
|
|
||||||
|
|
||||||
Shows a mini game where you have to defend against aliens.
|
|
||||||
|
|
||||||
What does it show you about pygame?
|
|
||||||
|
|
||||||
* pygame.sprite, the difference between Sprite and Group.
|
|
||||||
* dirty rectangle optimization for processing for speed.
|
|
||||||
* music with pygame.mixer.music, including fadeout
|
|
||||||
* sound effects with pygame.Sound
|
|
||||||
* event processing, keyboard handling, QUIT handling.
|
|
||||||
* a main loop frame limited with a game clock from the pygame.time module
|
|
||||||
* fullscreen switching.
|
|
||||||
|
|
||||||
|
|
||||||
Controls
|
|
||||||
--------
|
|
||||||
|
|
||||||
* Left and right arrows to move.
|
|
||||||
* Space bar to shoot.
|
|
||||||
* f key to toggle between fullscreen.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import random
|
|
||||||
import os
|
|
||||||
import pathlib
|
|
||||||
|
|
||||||
import pyscript
|
|
||||||
|
|
||||||
# import basic pygame modules
|
|
||||||
import pygame
|
|
||||||
|
|
||||||
# see if we can load more than standard BMP
|
|
||||||
if not pygame.image.get_extended():
|
|
||||||
msg = "Sorry, extended image module required"
|
|
||||||
raise SystemExit(msg)
|
|
||||||
|
|
||||||
|
|
||||||
# game constants
|
|
||||||
MAX_SHOTS = 2 # most player bullets onscreen
|
|
||||||
ALIEN_ODDS = 22 # chances a new alien appears
|
|
||||||
BOMB_ODDS = 60 # chances a new bomb will drop
|
|
||||||
ALIEN_RELOAD = 12 # frames between new aliens
|
|
||||||
SCREENRECT = pygame.Rect(0, 0, 640, 480)
|
|
||||||
SCORE = 0
|
|
||||||
|
|
||||||
|
|
||||||
main_dir = str(pathlib.Path(pygame.__file__).parent / "examples")
|
|
||||||
|
|
||||||
|
|
||||||
def load_image(file):
|
|
||||||
"""loads an image, prepares it for play"""
|
|
||||||
file = os.path.join(main_dir, "data", file)
|
|
||||||
try:
|
|
||||||
surface = pygame.image.load(file)
|
|
||||||
except pygame.error:
|
|
||||||
msg = f'Could not load image "{file}" {pygame.get_error()}'
|
|
||||||
raise SystemExit(msg)
|
|
||||||
return surface.convert()
|
|
||||||
|
|
||||||
|
|
||||||
def load_sound(file):
|
|
||||||
"""because pygame can be be compiled without mixer."""
|
|
||||||
if not pygame.mixer:
|
|
||||||
return None
|
|
||||||
file = os.path.join(main_dir, "data", file)
|
|
||||||
try:
|
|
||||||
return pygame.mixer.Sound(file)
|
|
||||||
except pygame.error:
|
|
||||||
print(f"Warning, unable to load, {file}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# Each type of game object gets an init and an update function.
|
|
||||||
# The update function is called once per frame, and it is when each object should
|
|
||||||
# change its current position and state.
|
|
||||||
#
|
|
||||||
# The Player object actually gets a "move" function instead of update,
|
|
||||||
# since it is passed extra information about the keyboard.
|
|
||||||
|
|
||||||
|
|
||||||
class Player(pygame.sprite.Sprite):
|
|
||||||
"""Representing the player as a moon buggy type car."""
|
|
||||||
|
|
||||||
speed = 10
|
|
||||||
bounce = 24
|
|
||||||
gun_offset = -11
|
|
||||||
images = []
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pygame.sprite.Sprite.__init__(self, self.containers)
|
|
||||||
self.image = self.images[0]
|
|
||||||
self.rect = self.image.get_rect(midbottom=SCREENRECT.midbottom)
|
|
||||||
self.reloading = False
|
|
||||||
self.origtop = self.rect.top
|
|
||||||
self.facing = -1
|
|
||||||
|
|
||||||
def move(self, direction):
|
|
||||||
if direction:
|
|
||||||
self.facing = direction
|
|
||||||
self.rect.move_ip(direction * self.speed, 0)
|
|
||||||
self.rect = self.rect.clamp(SCREENRECT)
|
|
||||||
if direction < 0:
|
|
||||||
self.image = self.images[0]
|
|
||||||
elif direction > 0:
|
|
||||||
self.image = self.images[1]
|
|
||||||
self.rect.top = self.origtop - (self.rect.left // self.bounce % 2)
|
|
||||||
|
|
||||||
def gunpos(self):
|
|
||||||
pos = self.facing * self.gun_offset + self.rect.centerx
|
|
||||||
return pos, self.rect.top
|
|
||||||
|
|
||||||
|
|
||||||
class Alien(pygame.sprite.Sprite):
|
|
||||||
"""An alien space ship. That slowly moves down the screen."""
|
|
||||||
|
|
||||||
speed = 13
|
|
||||||
animcycle = 12
|
|
||||||
images = []
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pygame.sprite.Sprite.__init__(self, self.containers)
|
|
||||||
self.image = self.images[0]
|
|
||||||
self.rect = self.image.get_rect()
|
|
||||||
self.facing = random.choice((-1, 1)) * Alien.speed
|
|
||||||
self.frame = 0
|
|
||||||
if self.facing < 0:
|
|
||||||
self.rect.right = SCREENRECT.right
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
self.rect.move_ip(self.facing, 0)
|
|
||||||
if not SCREENRECT.contains(self.rect):
|
|
||||||
self.facing = -self.facing
|
|
||||||
self.rect.top = self.rect.bottom + 1
|
|
||||||
self.rect = self.rect.clamp(SCREENRECT)
|
|
||||||
self.frame = self.frame + 1
|
|
||||||
self.image = self.images[self.frame // self.animcycle % 3]
|
|
||||||
|
|
||||||
|
|
||||||
class Explosion(pygame.sprite.Sprite):
|
|
||||||
"""An explosion. Hopefully the Alien and not the player!"""
|
|
||||||
|
|
||||||
defaultlife = 12
|
|
||||||
animcycle = 3
|
|
||||||
images = []
|
|
||||||
|
|
||||||
def __init__(self, actor):
|
|
||||||
pygame.sprite.Sprite.__init__(self, self.containers)
|
|
||||||
self.image = self.images[0]
|
|
||||||
self.rect = self.image.get_rect(center=actor.rect.center)
|
|
||||||
self.life = self.defaultlife
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""called every time around the game loop.
|
|
||||||
|
|
||||||
Show the explosion surface for 'defaultlife'.
|
|
||||||
Every game tick(update), we decrease the 'life'.
|
|
||||||
|
|
||||||
Also we animate the explosion.
|
|
||||||
"""
|
|
||||||
self.life = self.life - 1
|
|
||||||
self.image = self.images[self.life // self.animcycle % 2]
|
|
||||||
if self.life <= 0:
|
|
||||||
self.kill()
|
|
||||||
|
|
||||||
|
|
||||||
class Shot(pygame.sprite.Sprite):
|
|
||||||
"""a bullet the Player sprite fires."""
|
|
||||||
|
|
||||||
speed = -11
|
|
||||||
images = []
|
|
||||||
|
|
||||||
def __init__(self, pos):
|
|
||||||
pygame.sprite.Sprite.__init__(self, self.containers)
|
|
||||||
self.image = self.images[0]
|
|
||||||
self.rect = self.image.get_rect(midbottom=pos)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""called every time around the game loop.
|
|
||||||
|
|
||||||
Every tick we move the shot upwards.
|
|
||||||
"""
|
|
||||||
self.rect.move_ip(0, self.speed)
|
|
||||||
if self.rect.top <= 0:
|
|
||||||
self.kill()
|
|
||||||
|
|
||||||
|
|
||||||
class Bomb(pygame.sprite.Sprite):
|
|
||||||
"""A bomb the aliens drop."""
|
|
||||||
|
|
||||||
speed = 9
|
|
||||||
images = []
|
|
||||||
|
|
||||||
def __init__(self, alien):
|
|
||||||
pygame.sprite.Sprite.__init__(self, self.containers)
|
|
||||||
self.image = self.images[0]
|
|
||||||
self.rect = self.image.get_rect(midbottom=alien.rect.move(0, 5).midbottom)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""called every time around the game loop.
|
|
||||||
|
|
||||||
Every frame we move the sprite 'rect' down.
|
|
||||||
When it reaches the bottom we:
|
|
||||||
|
|
||||||
- make an explosion.
|
|
||||||
- remove the Bomb.
|
|
||||||
"""
|
|
||||||
self.rect.move_ip(0, self.speed)
|
|
||||||
if self.rect.bottom >= 470:
|
|
||||||
Explosion(self)
|
|
||||||
self.kill()
|
|
||||||
|
|
||||||
|
|
||||||
class Score(pygame.sprite.Sprite):
|
|
||||||
"""to keep track of the score."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pygame.sprite.Sprite.__init__(self)
|
|
||||||
self.font = pygame.Font(None, 20)
|
|
||||||
self.font.set_italic(1)
|
|
||||||
self.color = "white"
|
|
||||||
self.lastscore = -1
|
|
||||||
self.update()
|
|
||||||
self.rect = self.image.get_rect().move(10, 450)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""We only update the score in update() when it has changed."""
|
|
||||||
if self.lastscore != SCORE:
|
|
||||||
self.lastscore = SCORE
|
|
||||||
msg = "Score: %d" % SCORE
|
|
||||||
self.image = self.font.render(msg, 0, self.color)
|
|
||||||
|
|
||||||
|
|
||||||
async def main(winstyle=0):
|
|
||||||
# Initialize pygame
|
|
||||||
pygame.mixer.pre_init(44100, 32, 2, 1024)
|
|
||||||
pygame.init()
|
|
||||||
if pygame.mixer and not pygame.mixer.get_init():
|
|
||||||
print("Warning, no sound")
|
|
||||||
pygame.mixer = None
|
|
||||||
|
|
||||||
fullscreen = False
|
|
||||||
# Set the display mode
|
|
||||||
winstyle = 0 # |FULLSCREEN
|
|
||||||
screen = pygame.display.set_mode(SCREENRECT.size, winstyle)
|
|
||||||
|
|
||||||
# Load images, assign to sprite classes
|
|
||||||
# (do this before the classes are used, after screen setup)
|
|
||||||
img = load_image("player1.gif")
|
|
||||||
Player.images = [img, pygame.transform.flip(img, 1, 0)]
|
|
||||||
img = load_image("explosion1.gif")
|
|
||||||
Explosion.images = [img, pygame.transform.flip(img, 1, 1)]
|
|
||||||
Alien.images = [load_image(im) for im in ("alien1.gif", "alien2.gif", "alien3.gif")]
|
|
||||||
Bomb.images = [load_image("bomb.gif")]
|
|
||||||
Shot.images = [load_image("shot.gif")]
|
|
||||||
|
|
||||||
# decorate the game window
|
|
||||||
icon = pygame.transform.scale(Alien.images[0], (32, 32))
|
|
||||||
pygame.display.set_icon(icon)
|
|
||||||
pygame.display.set_caption("Pygame Aliens")
|
|
||||||
pygame.mouse.set_visible(0)
|
|
||||||
|
|
||||||
# create the background, tile the bgd image
|
|
||||||
bgdtile = load_image("background.gif")
|
|
||||||
background = pygame.Surface(SCREENRECT.size)
|
|
||||||
for x in range(0, SCREENRECT.width, bgdtile.get_width()):
|
|
||||||
background.blit(bgdtile, (x, 0))
|
|
||||||
screen.blit(background, (0, 0))
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
# load the sound effects
|
|
||||||
boom_sound = load_sound("boom.wav")
|
|
||||||
shoot_sound = load_sound("car_door.wav")
|
|
||||||
if pygame.mixer:
|
|
||||||
music = os.path.join(main_dir, "data", "house_lo.wav")
|
|
||||||
pygame.mixer.music.load(music)
|
|
||||||
pygame.mixer.music.play(-1)
|
|
||||||
|
|
||||||
# Initialize Game Groups
|
|
||||||
aliens = pygame.sprite.Group()
|
|
||||||
shots = pygame.sprite.Group()
|
|
||||||
bombs = pygame.sprite.Group()
|
|
||||||
all = pygame.sprite.RenderUpdates()
|
|
||||||
lastalien = pygame.sprite.GroupSingle()
|
|
||||||
|
|
||||||
# assign default groups to each sprite class
|
|
||||||
Player.containers = all
|
|
||||||
Alien.containers = aliens, all, lastalien
|
|
||||||
Shot.containers = shots, all
|
|
||||||
Bomb.containers = bombs, all
|
|
||||||
Explosion.containers = all
|
|
||||||
Score.containers = all
|
|
||||||
|
|
||||||
# Create Some Starting Values
|
|
||||||
global score
|
|
||||||
alienreload = ALIEN_RELOAD
|
|
||||||
_clock = pygame.Clock()
|
|
||||||
|
|
||||||
# initialize our starting sprites
|
|
||||||
global SCORE
|
|
||||||
player = Player()
|
|
||||||
Alien() # note, this 'lives' because it goes into a sprite group
|
|
||||||
if pygame.font:
|
|
||||||
all.add(Score())
|
|
||||||
|
|
||||||
# Run our main loop whilst the player is alive.
|
|
||||||
while player.alive():
|
|
||||||
# get input
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.QUIT:
|
|
||||||
return
|
|
||||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
|
|
||||||
return
|
|
||||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_f:
|
|
||||||
if not fullscreen:
|
|
||||||
print("Changing to FULLSCREEN")
|
|
||||||
screen_backup = screen.copy()
|
|
||||||
screen = pygame.display.set_mode(
|
|
||||||
SCREENRECT.size, winstyle | pygame.FULLSCREEN, bestdepth
|
|
||||||
)
|
|
||||||
screen.blit(screen_backup, (0, 0))
|
|
||||||
else:
|
|
||||||
print("Changing to windowed mode")
|
|
||||||
screen_backup = screen.copy()
|
|
||||||
screen = pygame.display.set_mode(
|
|
||||||
SCREENRECT.size, winstyle, bestdepth
|
|
||||||
)
|
|
||||||
screen.blit(screen_backup, (0, 0))
|
|
||||||
pygame.display.flip()
|
|
||||||
fullscreen = not fullscreen
|
|
||||||
|
|
||||||
keystate = pygame.key.get_pressed()
|
|
||||||
|
|
||||||
# clear/erase the last drawn sprites
|
|
||||||
all.clear(screen, background)
|
|
||||||
|
|
||||||
# update all the sprites
|
|
||||||
all.update()
|
|
||||||
|
|
||||||
# handle player input
|
|
||||||
direction = keystate[pygame.K_RIGHT] - keystate[pygame.K_LEFT]
|
|
||||||
player.move(direction)
|
|
||||||
firing = keystate[pygame.K_SPACE]
|
|
||||||
if not player.reloading and firing and len(shots) < MAX_SHOTS:
|
|
||||||
Shot(player.gunpos())
|
|
||||||
if pygame.mixer:
|
|
||||||
shoot_sound.play()
|
|
||||||
player.reloading = firing
|
|
||||||
|
|
||||||
# Create new alien
|
|
||||||
if alienreload:
|
|
||||||
alienreload = alienreload - 1
|
|
||||||
elif not int(random.random() * ALIEN_ODDS):
|
|
||||||
Alien()
|
|
||||||
alienreload = ALIEN_RELOAD
|
|
||||||
|
|
||||||
# Drop bombs
|
|
||||||
if lastalien and not int(random.random() * BOMB_ODDS):
|
|
||||||
Bomb(lastalien.sprite)
|
|
||||||
|
|
||||||
# Detect collisions between aliens and players.
|
|
||||||
for alien in pygame.sprite.spritecollide(player, aliens, 1):
|
|
||||||
if pygame.mixer:
|
|
||||||
boom_sound.play()
|
|
||||||
Explosion(alien)
|
|
||||||
Explosion(player)
|
|
||||||
SCORE = SCORE + 1
|
|
||||||
player.kill()
|
|
||||||
|
|
||||||
# See if shots hit the aliens.
|
|
||||||
for alien in pygame.sprite.groupcollide(aliens, shots, 1, 1):
|
|
||||||
if pygame.mixer:
|
|
||||||
boom_sound.play()
|
|
||||||
Explosion(alien)
|
|
||||||
SCORE = SCORE + 1
|
|
||||||
|
|
||||||
# See if alien bombs hit the player.
|
|
||||||
for bomb in pygame.sprite.spritecollide(player, bombs, 1):
|
|
||||||
if pygame.mixer:
|
|
||||||
boom_sound.play()
|
|
||||||
Explosion(player)
|
|
||||||
Explosion(bomb)
|
|
||||||
player.kill()
|
|
||||||
|
|
||||||
# draw the scene
|
|
||||||
dirty = all.draw(screen)
|
|
||||||
pygame.display.update(dirty)
|
|
||||||
|
|
||||||
# cap the framerate at 40fps. Also called 40HZ or 40 times per second.
|
|
||||||
await asyncio.sleep(0.025)
|
|
||||||
|
|
||||||
if pygame.mixer:
|
|
||||||
pygame.mixer.music.fadeout(1000)
|
|
||||||
|
|
||||||
|
|
||||||
main()
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="aliens.css" />
|
|
||||||
<link rel="stylesheet" href="../../../dist/core.css" />
|
|
||||||
<script type="module" src="../../../dist/core.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script type="py-game" src="aliens.py" config="./config.toml"></script>
|
|
||||||
<div class="demo">
|
|
||||||
<div class="demo-header">pygame.examples.aliens</div>
|
|
||||||
<div class="demo-content">
|
|
||||||
<canvas id="canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!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="py" config='{"packages":["jsonpointer==3.0.0"]}'>
|
|
||||||
print('Hello World')
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<!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="py" src="./main.py" terminal worker></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
print(input("What food would you like me to get from the shop? "))
|
|
||||||
Binary file not shown.
@@ -1,20 +0,0 @@
|
|||||||
from pyscript import config
|
|
||||||
|
|
||||||
MICROPYTHON = config["type"] == "mpy"
|
|
||||||
|
|
||||||
if MICROPYTHON:
|
|
||||||
def new(obj, *args, **kwargs):
|
|
||||||
return obj.new(*args, kwargs) if kwargs else obj.new(*args)
|
|
||||||
def call(obj, *args, **kwargs):
|
|
||||||
return obj(*args, kwargs) if kwargs else obj(*args)
|
|
||||||
else:
|
|
||||||
def new(obj, *args, **kwargs):
|
|
||||||
return obj.new(*args, **kwargs)
|
|
||||||
def call(obj, *args, **kwargs):
|
|
||||||
return obj(*args, **kwargs)
|
|
||||||
|
|
||||||
if not MICROPYTHON:
|
|
||||||
import pyodide_js
|
|
||||||
pyodide_js.setDebug(True)
|
|
||||||
|
|
||||||
from pyscript.ffi import to_js, create_proxy
|
|
||||||
Binary file not shown.
@@ -1,69 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>Genuary</title>
|
|
||||||
|
|
||||||
<!-- Recommended meta tags -->
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
|
||||||
|
|
||||||
<!-- PyScript CSS -->
|
|
||||||
<link rel="stylesheet" href="../../../dist/core.css">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: #4a315e;
|
|
||||||
color: white;
|
|
||||||
font-family: Inconsolata, Consolas, Monaco, Courier New;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gutter {
|
|
||||||
background-color: #eee;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gutter.gutter-vertical {
|
|
||||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
|
|
||||||
cursor: row-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
py-terminal {
|
|
||||||
max-height: 7em;
|
|
||||||
max-width: calc(100vw - 90px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#pyterm {
|
|
||||||
background-color: #191a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pyterm,
|
|
||||||
#threejs {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- This script tag bootstraps PyScript -->
|
|
||||||
<script type="importmap">
|
|
||||||
{
|
|
||||||
"imports": {
|
|
||||||
"three": "https://cdn.jsdelivr.net/npm/three@v0.173.0/build/three.module.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="module" src="../../../dist/core.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/split.js/1.6.5/split.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="stats"></div>
|
|
||||||
<div id="stats-off"></div>
|
|
||||||
<div class="split">
|
|
||||||
<div id="pyterm"></div>
|
|
||||||
<div id="threejs"></div>
|
|
||||||
</div>
|
|
||||||
<script type="py" src="./main.py" config="./pyscript.toml" async terminal></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
import sys
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BeatSync:
|
|
||||||
fft_res: int = field()
|
|
||||||
|
|
||||||
on_beat: bool = False
|
|
||||||
beat: int = -1
|
|
||||||
since_last_beat: float = sys.maxsize
|
|
||||||
|
|
||||||
_prev: int = 0
|
|
||||||
_count: int = 0
|
|
||||||
_bins: list[int] = field(default_factory=list)
|
|
||||||
_last_detection: float = -1.0
|
|
||||||
_threshold: int = 50
|
|
||||||
_diff: int = 40
|
|
||||||
_cooldown: float = 0.2
|
|
||||||
|
|
||||||
_highest: int = 0
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
self._bins = [int(13/16*self.fft_res/2)+17, int(13/16*self.fft_res/2)+18]
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.beat = -1
|
|
||||||
self._prev = 0
|
|
||||||
self._count = 0
|
|
||||||
self._last_detection = -1.0
|
|
||||||
self.since_last_beat = sys.maxsize
|
|
||||||
# print('bs reset')
|
|
||||||
|
|
||||||
def update(self, data, running_time):
|
|
||||||
self._count += 1
|
|
||||||
self.since_last_beat = running_time - self._last_detection
|
|
||||||
d = sum(data[bin] for bin in self._bins)
|
|
||||||
if d < self._threshold:
|
|
||||||
self.on_beat = False
|
|
||||||
elif d - self._prev < self._diff:
|
|
||||||
self.on_beat = False
|
|
||||||
elif self.since_last_beat < self._cooldown:
|
|
||||||
self.on_beat = False
|
|
||||||
else:
|
|
||||||
self._last_detection = running_time
|
|
||||||
self.since_last_beat = 0
|
|
||||||
self.on_beat = True
|
|
||||||
self.beat += 1
|
|
||||||
self._prev = d
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FreqIntensity:
|
|
||||||
freq: float = field()
|
|
||||||
fft_res: int = field()
|
|
||||||
|
|
||||||
intensity: float = 0.0
|
|
||||||
intensity_slew: float = 0.0
|
|
||||||
scale_min: float = 0.0
|
|
||||||
scale_max: float = 350
|
|
||||||
max: float = 0.0
|
|
||||||
_sample_rate: int = 48000
|
|
||||||
_bin_indexes: list[int] = field(default_factory=list)
|
|
||||||
_harmonics: int = 8
|
|
||||||
_slew_factor: float = 0.8
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
self._bin_indexes = [
|
|
||||||
round((harmonic+1) * self.freq / self._sample_rate * self.fft_res / 2)
|
|
||||||
for harmonic in range(self._harmonics)
|
|
||||||
]
|
|
||||||
print(self._bin_indexes)
|
|
||||||
|
|
||||||
def update(self, data):
|
|
||||||
intensity = 0.0
|
|
||||||
for bin in range(self._harmonics):
|
|
||||||
intensity += data[self._bin_indexes[bin]]/(bin+1)
|
|
||||||
self.intensity = intensity
|
|
||||||
self.intensity_slew = self._slew_factor * self.intensity_slew + (1 - self._slew_factor) * intensity
|
|
||||||
self.max = max(intensity, self.max)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def intensity_scaled(self):
|
|
||||||
raw = max(0, min(1.0, (self.intensity_slew - self.scale_min)/(self.scale_max - self.scale_min)))
|
|
||||||
return raw * raw
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from pyscript import document, window
|
|
||||||
|
|
||||||
from pyscript.js_modules import three as THREE
|
|
||||||
from pyscript.js_modules.stats_gl import default as StatsGL
|
|
||||||
from pyscript.js_modules import lsgeo, line2, linemat
|
|
||||||
|
|
||||||
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SoundPlayer:
|
|
||||||
sound: THREE.Audio = field()
|
|
||||||
on_start: Callable[[], None] = field()
|
|
||||||
on_stop: Callable[[], None] = field(default=lambda: None)
|
|
||||||
|
|
||||||
_start_time: float = -1.0
|
|
||||||
|
|
||||||
def play(self):
|
|
||||||
self.sound.stop()
|
|
||||||
self.on_start()
|
|
||||||
self._start_time = self.sound.context.currentTime
|
|
||||||
self.sound.play()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.sound.stop()
|
|
||||||
self.on_stop()
|
|
||||||
self._start_time = -1.0
|
|
||||||
|
|
||||||
def toggle(self):
|
|
||||||
if self.sound.isPlaying:
|
|
||||||
self.stop()
|
|
||||||
else:
|
|
||||||
self.play()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def running_time(self):
|
|
||||||
if self.sound.isPlaying:
|
|
||||||
return self.sound.context.currentTime - self._start_time
|
|
||||||
elif self._start_time != -1.0:
|
|
||||||
self.stop()
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
def get_renderer():
|
|
||||||
renderer = new(THREE.WebGLRenderer, antialias=True)
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
||||||
renderer.setPixelRatio(window.devicePixelRatio)
|
|
||||||
renderer.setClearColor(0xF5F0DC)
|
|
||||||
pyterms = list(document.getElementsByTagName("py-terminal"))
|
|
||||||
if pyterms:
|
|
||||||
pyterm = pyterms[0]
|
|
||||||
pyterm.parentNode.removeChild(pyterm)
|
|
||||||
document.getElementById("pyterm").appendChild(pyterm)
|
|
||||||
|
|
||||||
document.getElementById("threejs").appendChild(renderer.domElement)
|
|
||||||
|
|
||||||
initial = {0: "115px", 1: "calc(100vh - 120px)"}
|
|
||||||
@create_proxy
|
|
||||||
def split_element_style(dimension, size, gutter_size, index):
|
|
||||||
if index in initial:
|
|
||||||
result = {dimension: initial.pop(index)}
|
|
||||||
else:
|
|
||||||
result = {dimension: f"calc({int(size)}vh - {gutter_size}px)"}
|
|
||||||
return to_js(result)
|
|
||||||
|
|
||||||
call(
|
|
||||||
window.Split,
|
|
||||||
["#pyterm", "#threejs"],
|
|
||||||
direction="vertical",
|
|
||||||
elementStyle=split_element_style,
|
|
||||||
minSize=0,
|
|
||||||
maxSize=to_js([120, 10000]),
|
|
||||||
)
|
|
||||||
return renderer
|
|
||||||
|
|
||||||
def get_ortho_camera(view_size):
|
|
||||||
aspect_ratio = window.innerWidth / window.innerHeight
|
|
||||||
camera = new(
|
|
||||||
THREE.OrthographicCamera,
|
|
||||||
-view_size * aspect_ratio, # Left
|
|
||||||
view_size * aspect_ratio, # Right
|
|
||||||
view_size, # Top
|
|
||||||
-view_size, # Bottom
|
|
||||||
-view_size, # Near plane
|
|
||||||
view_size, # Far plane
|
|
||||||
)
|
|
||||||
camera.updateProjectionMatrix()
|
|
||||||
camera.position.set(0, 0, 0)
|
|
||||||
return camera
|
|
||||||
|
|
||||||
def get_loading_manager():
|
|
||||||
loading_mgr = new(THREE.LoadingManager)
|
|
||||||
ev = asyncio.Event()
|
|
||||||
|
|
||||||
@create_proxy
|
|
||||||
def on_start(url, itemsLoaded, itemsTotal):
|
|
||||||
print(f'[{itemsLoaded}/{itemsTotal}] Started loading file: {url}')
|
|
||||||
loading_mgr.onStart = on_start
|
|
||||||
|
|
||||||
@create_proxy
|
|
||||||
def on_progress(url, itemsLoaded, itemsTotal):
|
|
||||||
print(f'[{itemsLoaded}/{itemsTotal}] Loading file: {url}')
|
|
||||||
loading_mgr.onProgress = on_progress
|
|
||||||
|
|
||||||
@create_proxy
|
|
||||||
def on_error(url):
|
|
||||||
print(f'There was a problem loading {url}')
|
|
||||||
loading_mgr.onError = on_error
|
|
||||||
|
|
||||||
@create_proxy
|
|
||||||
def on_load():
|
|
||||||
print('Loading assets complete!')
|
|
||||||
ev.set()
|
|
||||||
loading_mgr.onLoad = on_load
|
|
||||||
|
|
||||||
return loading_mgr, ev
|
|
||||||
|
|
||||||
|
|
||||||
def get_perspective_camera():
|
|
||||||
aspect_ratio = window.innerWidth / window.innerHeight
|
|
||||||
camera = new(
|
|
||||||
THREE.PerspectiveCamera,
|
|
||||||
45, # fov
|
|
||||||
aspect_ratio,
|
|
||||||
0.25, # near plane
|
|
||||||
300, # far plane
|
|
||||||
)
|
|
||||||
camera.position.set(0, 0, 30)
|
|
||||||
return camera
|
|
||||||
|
|
||||||
def get_stats_gl(renderer):
|
|
||||||
stats = new(StatsGL, trackGPU=True, horizontal=False)
|
|
||||||
stats.init(renderer)
|
|
||||||
stats.dom.style.removeProperty("left")
|
|
||||||
stats.dom.style.right = "90px"
|
|
||||||
document.getElementById("stats").appendChild(stats.dom)
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def bg_from_v(*vertices):
|
|
||||||
geometry = new(THREE.BufferGeometry)
|
|
||||||
vertices_f32a = new(Float32Array, vertices)
|
|
||||||
attr = new(THREE.Float32BufferAttribute, vertices_f32a, 3)
|
|
||||||
return geometry.setAttribute('position', attr)
|
|
||||||
|
|
||||||
def bg_from_p(*points):
|
|
||||||
buf = new(THREE.BufferGeometry)
|
|
||||||
buf.setFromPoints(
|
|
||||||
[new(THREE.Vector3, p[0], p[1], p[2]) for p in points]
|
|
||||||
)
|
|
||||||
return buf
|
|
||||||
|
|
||||||
def clear():
|
|
||||||
# toggle stats and terminal?
|
|
||||||
stats_style = document.getElementById("stats-off").style
|
|
||||||
if stats_style.display == "none":
|
|
||||||
# turn stuff back on
|
|
||||||
stats_style.removeProperty("display")
|
|
||||||
document.getElementById("pyterm").style.height = "115px"
|
|
||||||
document.getElementById("threejs").style.height = "calc(100vh - 120px)"
|
|
||||||
for e in document.getElementsByClassName("gutter"):
|
|
||||||
e.style.removeProperty("display")
|
|
||||||
for e in document.getElementsByClassName("xterm-helper-textarea"):
|
|
||||||
e.focus()
|
|
||||||
break
|
|
||||||
return
|
|
||||||
|
|
||||||
# no longer focus on xterm
|
|
||||||
document.activeElement.blur()
|
|
||||||
# hide stats
|
|
||||||
document.getElementById("stats-off").style.display = "none"
|
|
||||||
# hide pyterm and split gutter
|
|
||||||
document.getElementById("pyterm").style.height = "0vh"
|
|
||||||
document.getElementById("threejs").style.height = "100vh"
|
|
||||||
for e in document.getElementsByClassName("gutter"):
|
|
||||||
e.style.display = "none"
|
|
||||||
# hide ltk ad
|
|
||||||
for e in document.getElementsByClassName("ltk-built-with"):
|
|
||||||
e.style.display = "none"
|
|
||||||
# hide pyscript ad
|
|
||||||
for e in document.getElementsByTagName("div"):
|
|
||||||
style = e.getAttribute("style")
|
|
||||||
if style and style.startswith("z-index:999"):
|
|
||||||
e.style.display = "none"
|
|
||||||
for e in document.getElementsByTagName("svg"):
|
|
||||||
style = e.getAttribute("style")
|
|
||||||
if style and style.startswith("z-index:999"):
|
|
||||||
e.style.display = "none"
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
print("Starting up...")
|
|
||||||
|
|
||||||
from array import array
|
|
||||||
import asyncio
|
|
||||||
import math
|
|
||||||
import time
|
|
||||||
|
|
||||||
from pyscript import document, window, PyWorker
|
|
||||||
|
|
||||||
from libthree import THREE, clear, SoundPlayer
|
|
||||||
from libthree import get_renderer, get_ortho_camera
|
|
||||||
from libthree import get_loading_manager, get_stats_gl
|
|
||||||
from libthree import lsgeo, line2, linemat, lsgeo
|
|
||||||
from libfft import BeatSync
|
|
||||||
|
|
||||||
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy
|
|
||||||
|
|
||||||
from js import Float32Array
|
|
||||||
|
|
||||||
scene = new(THREE.Scene)
|
|
||||||
|
|
||||||
view_size = 1
|
|
||||||
renderer = get_renderer()
|
|
||||||
camera = get_ortho_camera(view_size)
|
|
||||||
loading_mgr, loaded_event = get_loading_manager()
|
|
||||||
|
|
||||||
t_loader = new(THREE.TextureLoader, loading_mgr)
|
|
||||||
t_loader.setPath('assets/')
|
|
||||||
|
|
||||||
light = new(THREE.AmbientLight, 0xffffff, 1.0)
|
|
||||||
scene.add(light)
|
|
||||||
|
|
||||||
fft_res = 2048
|
|
||||||
audio_listener = new(THREE.AudioListener)
|
|
||||||
camera.add(audio_listener)
|
|
||||||
sound = new(THREE.Audio, audio_listener)
|
|
||||||
audio_loader = new(THREE.AudioLoader, loading_mgr)
|
|
||||||
analyser = new(THREE.AudioAnalyser, sound, fft_res)
|
|
||||||
|
|
||||||
@create_proxy
|
|
||||||
def on_audio_load(buffer):
|
|
||||||
sound.setBuffer(buffer)
|
|
||||||
sound.setVolume(0.9)
|
|
||||||
sound.setLoop(False)
|
|
||||||
|
|
||||||
audio_loader.load("assets/genuary25-18.m4a", on_audio_load)
|
|
||||||
|
|
||||||
spheres = new(THREE.Group)
|
|
||||||
scene.add(spheres)
|
|
||||||
|
|
||||||
line_basic_mat = new(
|
|
||||||
THREE.LineBasicMaterial,
|
|
||||||
color=0xffffff,
|
|
||||||
)
|
|
||||||
|
|
||||||
zero_mat = new(
|
|
||||||
linemat.LineMaterial,
|
|
||||||
color=0x662503,
|
|
||||||
linewidth=3,
|
|
||||||
)
|
|
||||||
|
|
||||||
other_mat = new(
|
|
||||||
linemat.LineMaterial,
|
|
||||||
color=0x662503,
|
|
||||||
linewidth=1.5,
|
|
||||||
)
|
|
||||||
|
|
||||||
grid_mat = new(
|
|
||||||
linemat.LineMaterial,
|
|
||||||
color=0x662503,
|
|
||||||
linewidth=1,
|
|
||||||
dashed=True,
|
|
||||||
dashScale=1,
|
|
||||||
dashSize=0.5,
|
|
||||||
gapSize=1,
|
|
||||||
dashOffset=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
lines = [new(THREE.Group), new(THREE.Group)]
|
|
||||||
scene.add(lines[0])
|
|
||||||
scene.add(lines[1])
|
|
||||||
|
|
||||||
def draw_lines(line_coords, mat_name, spy=False):
|
|
||||||
if spy:
|
|
||||||
line_coords_f32a = new(Float32Array, line_coords.length)
|
|
||||||
_it = line_coords.items
|
|
||||||
for i in range(line_coords.length):
|
|
||||||
line_coords_f32a[i] = _it[i]
|
|
||||||
else:
|
|
||||||
line_coords_f32a = new(Float32Array, line_coords)
|
|
||||||
if mat_name == 'zero':
|
|
||||||
mat = zero_mat
|
|
||||||
elif mat_name == 'grid':
|
|
||||||
mat = grid_mat
|
|
||||||
else:
|
|
||||||
mat = other_mat
|
|
||||||
|
|
||||||
geo = new(THREE.BufferGeometry)
|
|
||||||
geo.setAttribute('position', new(THREE.BufferAttribute, line_coords_f32a, 3))
|
|
||||||
seg = new(THREE.LineSegments, geo, line_basic_mat)
|
|
||||||
|
|
||||||
lsg = new(lsgeo.LineSegmentsGeometry)
|
|
||||||
lsg.fromLineSegments(seg)
|
|
||||||
l1 = new(line2.Line2, lsg, mat)
|
|
||||||
l1.computeLineDistances()
|
|
||||||
l2 = new(line2.Line2, lsg, mat)
|
|
||||||
l2.computeLineDistances()
|
|
||||||
lines[0].add(l1)
|
|
||||||
lines[1].add(l2)
|
|
||||||
|
|
||||||
seg.geometry.dispose()
|
|
||||||
del geo
|
|
||||||
del seg
|
|
||||||
|
|
||||||
def drawing_done():
|
|
||||||
maybe_with_spy = "with SPy" if USE_SPY else "with pure Python"
|
|
||||||
print(f"Time elapsed computing {maybe_with_spy}:", time.time() - start_ts)
|
|
||||||
drawing_event.set()
|
|
||||||
|
|
||||||
grid_width = 0
|
|
||||||
grid_height = 0
|
|
||||||
scroll_offset = 0
|
|
||||||
def scale_lines(grid_ws=None, grid_hs=None, offset=None):
|
|
||||||
global grid_width, grid_height, scroll_offset
|
|
||||||
|
|
||||||
if grid_ws:
|
|
||||||
grid_width = grid_ws
|
|
||||||
else:
|
|
||||||
grid_ws = grid_width
|
|
||||||
|
|
||||||
if grid_hs:
|
|
||||||
grid_height = grid_hs
|
|
||||||
else:
|
|
||||||
grid_hs = grid_height
|
|
||||||
|
|
||||||
if offset:
|
|
||||||
scroll_offset = offset
|
|
||||||
else:
|
|
||||||
offset = scroll_offset
|
|
||||||
|
|
||||||
scale = 2.04/grid_hs
|
|
||||||
lines[0].scale.set(scale, scale, scale)
|
|
||||||
lines[1].scale.set(scale, scale, scale)
|
|
||||||
lines[0].position.set((offset - grid_ws/2) * scale, -grid_hs/2 * scale, 0)
|
|
||||||
lines[1].position.set((offset + grid_ws/2) * scale, -grid_hs/2 * scale, 0)
|
|
||||||
|
|
||||||
def append_p(lines, p1, p2):
|
|
||||||
lines.append(p1[0])
|
|
||||||
lines.append(p1[1])
|
|
||||||
lines.append(0)
|
|
||||||
lines.append(p2[0])
|
|
||||||
lines.append(p2[1])
|
|
||||||
lines.append(0)
|
|
||||||
|
|
||||||
def initial_calc():
|
|
||||||
grid_w = int(1920 * 4)
|
|
||||||
grid_h = 1080 * 2
|
|
||||||
grid_scale = 10
|
|
||||||
noise_factor = 500
|
|
||||||
grid_hs = int(grid_h/grid_scale)
|
|
||||||
grid_ws = int(grid_w/grid_scale)
|
|
||||||
crossfade_range = int(grid_ws/12.5)
|
|
||||||
|
|
||||||
def grid_lines():
|
|
||||||
lines = array("d")
|
|
||||||
grid_goal = 24
|
|
||||||
grid_size_i = int(round((grid_ws - crossfade_range) / grid_goal))
|
|
||||||
grid_actual = (grid_ws - crossfade_range) / grid_size_i
|
|
||||||
for i in range(0, grid_size_i):
|
|
||||||
x = i * grid_actual
|
|
||||||
append_p(lines, (x, 0), (x, grid_hs))
|
|
||||||
for y in range(0, grid_hs, grid_goal):
|
|
||||||
append_p(lines, (0, y), (grid_ws-crossfade_range, y))
|
|
||||||
return lines
|
|
||||||
|
|
||||||
import perlin
|
|
||||||
spy_perlin = perlin.lib
|
|
||||||
spy_perlin.init()
|
|
||||||
spy_perlin.seed(44)
|
|
||||||
scale_lines(grid_ws - crossfade_range, grid_hs)
|
|
||||||
print("Computing the height map")
|
|
||||||
spy_perlin.make_height_map(grid_ws, grid_hs)
|
|
||||||
spy_perlin.update_height_map(grid_ws, grid_hs, grid_scale / noise_factor, 0)
|
|
||||||
print("Cross-fading the height map")
|
|
||||||
spy_perlin.crossfade_height_map(grid_ws, grid_hs, crossfade_range)
|
|
||||||
print("Drawing grid")
|
|
||||||
draw_lines(grid_lines(), 'grid')
|
|
||||||
print("Marching squares")
|
|
||||||
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0), 'zero', spy=True)
|
|
||||||
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.3), 'positive', spy=True)
|
|
||||||
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.3), 'negative', spy=True)
|
|
||||||
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.45), 'positive', spy=True)
|
|
||||||
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.45), 'negative', spy=True)
|
|
||||||
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.6), 'positive', spy=True)
|
|
||||||
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.6), 'negative', spy=True)
|
|
||||||
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.8), 'negative', spy=True)
|
|
||||||
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.8), 'positive', spy=True)
|
|
||||||
drawing_done()
|
|
||||||
|
|
||||||
drawing_event = asyncio.Event()
|
|
||||||
start_ts = time.time()
|
|
||||||
|
|
||||||
USE_SPY = True
|
|
||||||
if USE_SPY:
|
|
||||||
initial_calc()
|
|
||||||
else:
|
|
||||||
worker = PyWorker("./worker.py", type="pyodide", configURL="./pyscript.toml")
|
|
||||||
worker.sync.draw_lines = draw_lines
|
|
||||||
worker.sync.drawing_done = drawing_done
|
|
||||||
worker.sync.scale_lines = scale_lines
|
|
||||||
worker.sync.print = print
|
|
||||||
|
|
||||||
@create_proxy
|
|
||||||
def on_tap(event):
|
|
||||||
clear()
|
|
||||||
player.toggle()
|
|
||||||
document.addEventListener("click", on_tap)
|
|
||||||
|
|
||||||
@create_proxy
|
|
||||||
def on_key_down(event):
|
|
||||||
element = document.activeElement
|
|
||||||
_class = element.getAttribute("class")
|
|
||||||
in_xterm = element.tagName != "BODY" and _class and "xterm" in _class
|
|
||||||
|
|
||||||
if event.code == "Backquote":
|
|
||||||
# Screenshot mode.
|
|
||||||
clear()
|
|
||||||
elif not in_xterm:
|
|
||||||
# Don't react to those bindings when typing code.
|
|
||||||
if event.code == "Space":
|
|
||||||
player.toggle()
|
|
||||||
document.addEventListener("keydown", on_key_down)
|
|
||||||
|
|
||||||
@create_proxy
|
|
||||||
def on_window_resize(event):
|
|
||||||
aspect_ratio = window.innerWidth / window.innerHeight
|
|
||||||
if camera.type == "OrthographicCamera":
|
|
||||||
camera.left = -view_size * aspect_ratio
|
|
||||||
camera.right = view_size * aspect_ratio
|
|
||||||
camera.top = view_size
|
|
||||||
camera.bottom = -view_size
|
|
||||||
camera.updateProjectionMatrix()
|
|
||||||
elif camera.type == "PerspectiveCamera":
|
|
||||||
camera.aspect = window.innerWidth / window.innerHeight
|
|
||||||
camera.updateProjectionMatrix()
|
|
||||||
else:
|
|
||||||
raise ValueError("Unknown camera type")
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
||||||
scale_lines()
|
|
||||||
|
|
||||||
window.addEventListener("resize", on_window_resize)
|
|
||||||
|
|
||||||
@create_proxy
|
|
||||||
def animate(now=0.0):
|
|
||||||
data = analyser.getFrequencyData()#.to_py() in Pyodide
|
|
||||||
audio_now = player.running_time
|
|
||||||
bs.update(data, audio_now)
|
|
||||||
|
|
||||||
if grid_width:
|
|
||||||
offset = -((20 * audio_now) % grid_width)
|
|
||||||
scale_lines(offset=offset)
|
|
||||||
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
stats_gl.update()
|
|
||||||
|
|
||||||
def reset():
|
|
||||||
global scroll_offset
|
|
||||||
bs.reset()
|
|
||||||
scale_lines()
|
|
||||||
|
|
||||||
def on_stop():
|
|
||||||
global scroll_offset
|
|
||||||
bs.reset()
|
|
||||||
scale_lines()
|
|
||||||
|
|
||||||
await loaded_event.wait()
|
|
||||||
|
|
||||||
stats_gl = get_stats_gl(renderer)
|
|
||||||
player = SoundPlayer(sound=sound, on_start=reset, on_stop=on_stop)
|
|
||||||
bs = BeatSync(fft_res=fft_res)
|
|
||||||
renderer.setAnimationLoop(animate)
|
|
||||||
print("Waiting for the contours...")
|
|
||||||
|
|
||||||
await drawing_event.wait()
|
|
||||||
print("Tap the map to start...")
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
# Translated from https://github.com/josephg/noisejs.
|
|
||||||
from libthree import THREE
|
|
||||||
from multipyjs import new
|
|
||||||
|
|
||||||
class V3:
|
|
||||||
def __init__(self, x, y, z):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.z = z
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"V3({self.x}, {self.y}, {self.z})"
|
|
||||||
|
|
||||||
def dot2(self, x, y):
|
|
||||||
return self.x * x + self.y * y
|
|
||||||
|
|
||||||
def dot3(self, x, y, z):
|
|
||||||
return self.x * x + self.y * y + self.z * z
|
|
||||||
|
|
||||||
def to_js(self, scale=1.0):
|
|
||||||
return new(THREE.Vector3, self.x * scale, self.y * scale, self.z * scale)
|
|
||||||
|
|
||||||
PERM = [0] * 512
|
|
||||||
V3_P = [0] * 512 # assigned V3s in seed()
|
|
||||||
P = [151, 160, 137, 91, 90, 15,
|
|
||||||
131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23,
|
|
||||||
190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
|
|
||||||
88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
|
|
||||||
77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244,
|
|
||||||
102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196,
|
|
||||||
135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123,
|
|
||||||
5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42,
|
|
||||||
223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
|
|
||||||
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228,
|
|
||||||
251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107,
|
|
||||||
49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
|
|
||||||
138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180]
|
|
||||||
V3_I = [V3(1, 1, 0), V3(-1, 1, 0), V3(1, -1, 0), V3(-1, -1, 0),
|
|
||||||
V3(1, 0, 1), V3(-1, 0, 1), V3(1, 0, -1), V3(-1, 0, -1),
|
|
||||||
V3(0, 1, 1), V3(0, -1, 1), V3(0, 1, -1), V3(0, -1, -1)]
|
|
||||||
|
|
||||||
def seed(s):
|
|
||||||
if isinstance(s, float) and 0.0 < s < 1.0:
|
|
||||||
s *= 65536
|
|
||||||
|
|
||||||
s = int(s)
|
|
||||||
if s < 256:
|
|
||||||
s |= s << 8
|
|
||||||
|
|
||||||
for i in range(256):
|
|
||||||
if i & 1:
|
|
||||||
v = P[i] ^ (s & 255)
|
|
||||||
else:
|
|
||||||
v = P[i] ^ ((s >> 8) & 255)
|
|
||||||
|
|
||||||
PERM[i] = PERM[i + 256] = v
|
|
||||||
V3_P[i] = V3_P[i + 256] = V3_I[v % 12]
|
|
||||||
|
|
||||||
seed(0)
|
|
||||||
|
|
||||||
def fade(t):
|
|
||||||
return t * t * t * (t * (t * 6 - 15) + 10)
|
|
||||||
|
|
||||||
def lerp(a, b, t):
|
|
||||||
return (1 - t) * a + t * b
|
|
||||||
|
|
||||||
def perlin3(x, y, z):
|
|
||||||
# grid cells
|
|
||||||
x_c = int(x)
|
|
||||||
y_c = int(y)
|
|
||||||
z_c = int(z)
|
|
||||||
# relative coords within the cell
|
|
||||||
x -= x_c
|
|
||||||
y -= y_c
|
|
||||||
z -= z_c
|
|
||||||
# wrap cells
|
|
||||||
x_c &= 255
|
|
||||||
y_c &= 255
|
|
||||||
z_c &= 255
|
|
||||||
# noise contributions to corners
|
|
||||||
n000 = V3_P[x_c + PERM[y_c + PERM[z_c]]].dot3(x, y, z)
|
|
||||||
n001 = V3_P[x_c + PERM[y_c + PERM[z_c + 1]]].dot3(x, y, z - 1)
|
|
||||||
n010 = V3_P[x_c + PERM[y_c + 1 + PERM[z_c]]].dot3(x, y - 1, z)
|
|
||||||
n011 = V3_P[x_c + PERM[y_c + 1 + PERM[z_c + 1]]].dot3(x, y - 1, z - 1)
|
|
||||||
n100 = V3_P[x_c + 1 + PERM[y_c + PERM[z_c]]].dot3(x - 1, y, z)
|
|
||||||
n101 = V3_P[x_c + 1 + PERM[y_c + PERM[z_c + 1]]].dot3(x - 1, y, z - 1)
|
|
||||||
n110 = V3_P[x_c + 1 + PERM[y_c + 1 + PERM[z_c]]].dot3(x - 1, y - 1, z)
|
|
||||||
n111 = V3_P[x_c + 1 + PERM[y_c + 1 + PERM[z_c + 1]]].dot3(x - 1, y - 1, z - 1)
|
|
||||||
# fade curve
|
|
||||||
u = fade(x)
|
|
||||||
v = fade(y)
|
|
||||||
w = fade(z)
|
|
||||||
# interpolation
|
|
||||||
return lerp(
|
|
||||||
lerp(lerp(n000, n100, u), lerp(n001, n101, u), w),
|
|
||||||
lerp(lerp(n010, n110, u), lerp(n011, n111, u), w),
|
|
||||||
v,
|
|
||||||
)
|
|
||||||
|
|
||||||
def curl2(x, y, z):
|
|
||||||
# https://www.bit-101.com/2017/2021/07/curl-noise/
|
|
||||||
delta = 0.01
|
|
||||||
n1 = perlin3(x + delta, y, z)
|
|
||||||
n2 = perlin3(x - delta, y, z)
|
|
||||||
cy = -(n1 - n2) / (delta * 2)
|
|
||||||
n1 = perlin3(x, y + delta, z)
|
|
||||||
n2 = perlin3(x, y - delta, z)
|
|
||||||
cx = -(n1 - n2) / (delta * 2)
|
|
||||||
print(n1, n2)
|
|
||||||
return V3(cx, cy, 0)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
name = "Marching Squares with SPy Copy Copy"
|
|
||||||
packages = [ "cffi", "./glue/perlin-0.0.0-cp312-cp312-pyodide_2024_0_wasm32.whl",]
|
|
||||||
|
|
||||||
[files]
|
|
||||||
"./libthree.py" = ""
|
|
||||||
"./libfft.py" = ""
|
|
||||||
"./perlin_py.py" = ""
|
|
||||||
"./worker.py" = ""
|
|
||||||
"./glue/multipyjs.py" = "./multipyjs.py"
|
|
||||||
|
|
||||||
[js_modules.main]
|
|
||||||
"https://cdn.jsdelivr.net/npm/three@v0.173.0/build/three.module.js" = "three"
|
|
||||||
"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/LineMaterial.js" = "linemat"
|
|
||||||
"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/Line2.js" = "line2"
|
|
||||||
"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/LineSegmentsGeometry.js" = "lsgeo"
|
|
||||||
"https://cdn.jsdelivr.net/npm/stats-gl@3.6.0/dist/main.js" = "stats_gl"
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
from array import array
|
|
||||||
|
|
||||||
from pyscript import sync, window
|
|
||||||
from perlin_py import perlin3, seed
|
|
||||||
|
|
||||||
grid_w = int(1920 * 4)
|
|
||||||
grid_h = 1080 * 2
|
|
||||||
grid_scale = 10
|
|
||||||
noise_factor = 500
|
|
||||||
grid_hs = int(grid_h/grid_scale)
|
|
||||||
grid_ws = int(grid_w/grid_scale)
|
|
||||||
crossfade_range = int(grid_ws/12.5)
|
|
||||||
height_map = array("d", [0.0] * (grid_hs * grid_ws))
|
|
||||||
edge_table = [
|
|
||||||
(), # 0
|
|
||||||
((3, 2),), # 1
|
|
||||||
((2, 1),), # 2
|
|
||||||
((3, 1),), # 3
|
|
||||||
((0, 1),), # 4
|
|
||||||
((0, 3), (1, 2)), # 5 (ambiguous)
|
|
||||||
((0, 2),), # 6
|
|
||||||
((0, 3),), # 7
|
|
||||||
((0, 3),), # 8
|
|
||||||
((0, 2),), # 9
|
|
||||||
((0, 1), (2, 3)), # 10 (ambiguous)
|
|
||||||
((0, 1),), # 11
|
|
||||||
((3, 1),), # 12
|
|
||||||
((2, 1),), # 13
|
|
||||||
((3, 2),), # 14
|
|
||||||
(), # 15
|
|
||||||
]
|
|
||||||
|
|
||||||
def update_height_map(z):
|
|
||||||
i = 0
|
|
||||||
for y in range(0, grid_h, grid_scale):
|
|
||||||
for x in range(0, grid_w, grid_scale):
|
|
||||||
# 3 octaves of noise
|
|
||||||
n = perlin3(x/noise_factor, y/noise_factor, z)
|
|
||||||
n += 0.50 * perlin3(2*x/noise_factor, 2*y/noise_factor, z)
|
|
||||||
n += 0.25 * perlin3(4*x/noise_factor, 4*y/noise_factor, z)
|
|
||||||
height_map[i] = n
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
def crossfade_height_map():
|
|
||||||
for y in range(grid_hs):
|
|
||||||
for x in range(crossfade_range):
|
|
||||||
pos_i = y*grid_ws + x
|
|
||||||
neg_i = y*grid_ws + grid_ws - crossfade_range + x
|
|
||||||
weight = x/crossfade_range
|
|
||||||
old_pos = height_map[pos_i]
|
|
||||||
old_neg = height_map[neg_i]
|
|
||||||
height_map[neg_i] = height_map[pos_i] = weight * old_pos + (1.0 - weight) * old_neg
|
|
||||||
|
|
||||||
|
|
||||||
def _crossfade_height_map():
|
|
||||||
for y in range(grid_hs):
|
|
||||||
for x in range(crossfade_range):
|
|
||||||
pos_i = y*grid_ws + x
|
|
||||||
neg_i = y*grid_ws + grid_ws - x - 1
|
|
||||||
old_pos = height_map[pos_i]
|
|
||||||
old_neg = height_map[neg_i]
|
|
||||||
weight = 0.5 - x/crossfade_range/2
|
|
||||||
height_map[pos_i] = (1.0 - weight) * old_pos + weight * old_neg
|
|
||||||
height_map[neg_i] = (1.0 - weight) * old_neg + weight * old_pos
|
|
||||||
|
|
||||||
def interpolate(sq_threshold, v1, v2):
|
|
||||||
if v1 == v2:
|
|
||||||
return v1
|
|
||||||
return (sq_threshold - v1) / (v2 - v1)
|
|
||||||
|
|
||||||
stats = {'maxx': 0, 'maxy': 0, 'minx': 0, 'miny': 0}
|
|
||||||
def append_p(lines, p1, p2):
|
|
||||||
lines.append(p1[0])
|
|
||||||
lines.append(p1[1])
|
|
||||||
lines.append(0)
|
|
||||||
lines.append(p2[0])
|
|
||||||
lines.append(p2[1])
|
|
||||||
lines.append(0)
|
|
||||||
stats['maxy'] = max(p1[1], p2[1], stats['maxy'])
|
|
||||||
stats['miny'] = min(p1[1], p2[1], stats['miny'])
|
|
||||||
stats['maxx'] = max(p1[0], p2[0], stats['maxx'])
|
|
||||||
stats['minx'] = min(p1[0], p2[0], stats['minx'])
|
|
||||||
|
|
||||||
def marching_squares(height_map, sq_threshold):
|
|
||||||
lines = array("d")
|
|
||||||
|
|
||||||
for y in range(grid_hs-1):
|
|
||||||
for x in range(grid_ws-1): #cf
|
|
||||||
tl = height_map[y*grid_ws + x]
|
|
||||||
tr = height_map[y*grid_ws + x+1]
|
|
||||||
bl = height_map[(y+1)*grid_ws + x]
|
|
||||||
br = height_map[(y+1)*grid_ws + x+1]
|
|
||||||
|
|
||||||
sq_idx = 0
|
|
||||||
if tl > sq_threshold:
|
|
||||||
sq_idx |= 8
|
|
||||||
if tr > sq_threshold:
|
|
||||||
sq_idx |= 4
|
|
||||||
if br > sq_threshold:
|
|
||||||
sq_idx |= 2
|
|
||||||
if bl > sq_threshold:
|
|
||||||
sq_idx |= 1
|
|
||||||
|
|
||||||
edge_points = [
|
|
||||||
(x + interpolate(sq_threshold, tl, tr), y),
|
|
||||||
(x + 1, y + interpolate(sq_threshold, tr, br)),
|
|
||||||
(x + interpolate(sq_threshold, bl, br), y + 1),
|
|
||||||
(x, y + interpolate(sq_threshold, tl, bl)),
|
|
||||||
]
|
|
||||||
|
|
||||||
for a, b in edge_table[sq_idx]:
|
|
||||||
append_p(lines, edge_points[a], edge_points[b])
|
|
||||||
|
|
||||||
return lines
|
|
||||||
|
|
||||||
def grid_lines():
|
|
||||||
lines = array("d")
|
|
||||||
for x in range(0, grid_ws - crossfade_range, 26):
|
|
||||||
append_p(lines, (x, 0), (x, grid_hs))
|
|
||||||
for y in range(0, grid_hs, 24):
|
|
||||||
append_p(lines, (0, y), (grid_ws-crossfade_range, y))
|
|
||||||
return lines
|
|
||||||
|
|
||||||
seed(44)
|
|
||||||
sync.scale_lines(grid_ws - crossfade_range, grid_hs)
|
|
||||||
sync.print("Computing the height map")
|
|
||||||
update_height_map(0)
|
|
||||||
sync.print("Cross-fading the height map")
|
|
||||||
crossfade_height_map()
|
|
||||||
sync.draw_lines(grid_lines(), 'grid')
|
|
||||||
sync.draw_lines(marching_squares(height_map, 0), 'zero')
|
|
||||||
sync.draw_lines(marching_squares(height_map, 0.3), 'positive')
|
|
||||||
sync.draw_lines(marching_squares(height_map, -0.3), 'negative')
|
|
||||||
sync.draw_lines(marching_squares(height_map, 0.45), 'positive')
|
|
||||||
sync.draw_lines(marching_squares(height_map, -0.45), 'negative')
|
|
||||||
sync.draw_lines(marching_squares(height_map, 0.6), 'positive')
|
|
||||||
sync.draw_lines(marching_squares(height_map, -0.6), 'negative')
|
|
||||||
sync.draw_lines(marching_squares(height_map, -0.8), 'negative')
|
|
||||||
sync.draw_lines(marching_squares(height_map, 0.8), 'positive')
|
|
||||||
print(stats)
|
|
||||||
sync.drawing_done()
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="../../../dist/core.css">
|
|
||||||
<script type="module" src="../../../dist/core.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="status">Status:</div>
|
|
||||||
<canvas id="canvas" width="200" height="200"></canvas>
|
|
||||||
<script type="py-game" src="./main.py" config="./pyscript.toml"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import sys
|
|
||||||
print("Starting test...")
|
|
||||||
|
|
||||||
# Try NumPy
|
|
||||||
try:
|
|
||||||
import numpy as np
|
|
||||||
arr = np.array([1, 2, 3])
|
|
||||||
print(f"NumPy works: {arr.mean()}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"NumPy error: {e}")
|
|
||||||
|
|
||||||
# Try PyGame without NumPy first
|
|
||||||
try:
|
|
||||||
print("Testing PyGame...")
|
|
||||||
import pygame
|
|
||||||
screen = pygame.display.set_mode((200, 200))
|
|
||||||
screen.fill((255, 0, 0)) # Fill with red
|
|
||||||
pygame.display.flip()
|
|
||||||
print("PyGame works!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"PyGame error: {e}")
|
|
||||||
|
|
||||||
# Now try PyGame with NumPy
|
|
||||||
try:
|
|
||||||
print("Testing PyGame+NumPy...")
|
|
||||||
color_array = np.random.randint(0, 255, size=(50, 50, 3), dtype=np.uint8)
|
|
||||||
surface = pygame.surfarray.make_surface(color_array)
|
|
||||||
screen.blit(surface, (75, 75))
|
|
||||||
pygame.display.flip()
|
|
||||||
print("PyGame+NumPy integration works!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"PyGame+NumPy integration error: {e}")
|
|
||||||
|
|
||||||
print("Test completed")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
name = "PyGame Numpy Minimal Example Copy"
|
|
||||||
packages = [ "numpy", ]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
const timeout = 60 * 1000;
|
|
||||||
|
|
||||||
test.setTimeout(timeout);
|
|
||||||
|
|
||||||
test('Python unit tests - MicroPython on MAIN thread', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:8080/tests/python/index.html');
|
|
||||||
const result = page.locator("#result"); // Payload for results will be here.
|
|
||||||
await result.waitFor({ timeout }); // wait for the result.
|
|
||||||
const data = JSON.parse(await result.textContent()); // get the result data.
|
|
||||||
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Python unit tests - Pyodide on MAIN thread', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:8080/tests/python/index.html?type=py');
|
|
||||||
const result = page.locator("#result"); // Payload for results will be here.
|
|
||||||
await result.waitFor({ timeout }); // wait for the result.
|
|
||||||
const data = JSON.parse(await result.textContent()); // get the result data.
|
|
||||||
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
|
||||||
});
|
|
||||||
35
core/tests/py_tests.spec.js
Normal file
35
core/tests/py_tests.spec.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.setTimeout(120 * 1000);
|
||||||
|
|
||||||
|
test('Python unit tests - MicroPython on MAIN thread', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080/tests/python/index.html');
|
||||||
|
const result = page.locator("#result"); // Payload for results will be here.
|
||||||
|
await result.waitFor(); // wait for the result.
|
||||||
|
const data = JSON.parse(await result.textContent()); // get the result data.
|
||||||
|
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Python unit tests - Pyodide on MAIN thread', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080/tests/python/index.html?type=py');
|
||||||
|
const result = page.locator("#result"); // Payload for results will be here.
|
||||||
|
await result.waitFor(); // wait for the result.
|
||||||
|
const data = JSON.parse(await result.textContent()); // get the result data.
|
||||||
|
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Python unit tests - MicroPython on WORKER', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080/tests/python/index.html?worker');
|
||||||
|
const result = page.locator("#result"); // Payload for results will be here.
|
||||||
|
await result.waitFor(); // wait for the result.
|
||||||
|
const data = JSON.parse(await result.textContent()); // get the result data.
|
||||||
|
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Python unit tests - Pyodide on WORKER', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080/tests/python/index.html?type=py&worker');
|
||||||
|
const result = page.locator("#result"); // Payload for results will be here.
|
||||||
|
await result.waitFor(); // wait for the result.
|
||||||
|
const data = JSON.parse(await result.textContent()); // get the result data.
|
||||||
|
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
||||||
|
});
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
const timeout = 120 * 1000;
|
|
||||||
|
|
||||||
test.setTimeout(timeout);
|
|
||||||
|
|
||||||
test('Python unit tests - MicroPython on WORKER', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:8080/tests/python/index.html?worker');
|
|
||||||
const result = page.locator("#result"); // Payload for results will be here.
|
|
||||||
await result.waitFor({ timeout }); // wait for the result.
|
|
||||||
const data = JSON.parse(await result.textContent()); // get the result data.
|
|
||||||
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Python unit tests - Pyodide on WORKER', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:8080/tests/python/index.html?type=py&worker');
|
|
||||||
const result = page.locator("#result"); // Payload for results will be here.
|
|
||||||
await result.waitFor({ timeout }); // wait for the result.
|
|
||||||
const data = JSON.parse(await result.textContent()); // get the result data.
|
|
||||||
await expect(data.fails).toMatchObject([]); // ensure no test failed.
|
|
||||||
});
|
|
||||||
@@ -62,7 +62,6 @@
|
|||||||
<button id="a-test-button">I'm a button to be clicked</button>
|
<button id="a-test-button">I'm a button to be clicked</button>
|
||||||
<button>I'm another button you can click</button>
|
<button>I'm another button you can click</button>
|
||||||
<button id="a-third-button">2 is better than 3 :)</button>
|
<button id="a-third-button">2 is better than 3 :)</button>
|
||||||
<button id="another-test-button">I'm another button to be clicked</button>
|
|
||||||
|
|
||||||
<div id="element-append-tests"></div>
|
<div id="element-append-tests"></div>
|
||||||
<p class="collection"></p>
|
<p class="collection"></p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"https://raw.githubusercontent.com/ntoll/upytest/1.0.9/upytest.py": "",
|
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/upytest.py": "",
|
||||||
"./tests/test_config.py": "tests/test_config.py",
|
"./tests/test_config.py": "tests/test_config.py",
|
||||||
"./tests/test_current_target.py": "tests/test_current_target.py",
|
"./tests/test_current_target.py": "tests/test_current_target.py",
|
||||||
"./tests/test_display.py": "tests/test_display.py",
|
"./tests/test_display.py": "tests/test_display.py",
|
||||||
@@ -8,12 +8,11 @@
|
|||||||
"./tests/test_fetch.py": "tests/test_fetch.py",
|
"./tests/test_fetch.py": "tests/test_fetch.py",
|
||||||
"./tests/test_ffi.py": "tests/test_ffi.py",
|
"./tests/test_ffi.py": "tests/test_ffi.py",
|
||||||
"./tests/test_js_modules.py": "tests/test_js_modules.py",
|
"./tests/test_js_modules.py": "tests/test_js_modules.py",
|
||||||
"./tests/test_media.py": "tests/test_media.py",
|
|
||||||
"./tests/test_storage.py": "tests/test_storage.py",
|
"./tests/test_storage.py": "tests/test_storage.py",
|
||||||
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
|
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
|
||||||
"./tests/test_web.py": "tests/test_web.py",
|
"./tests/test_web.py": "tests/test_web.py",
|
||||||
"./tests/test_websocket.py": "tests/test_websocket.py",
|
"./tests/test_websocket.py": "tests/test_websocket.py",
|
||||||
"./tests/test_events.py": "tests/test_events.py",
|
"./tests/test_when.py": "tests/test_when.py",
|
||||||
"./tests/test_window.py": "tests/test_window.py"
|
"./tests/test_window.py": "tests/test_window.py"
|
||||||
},
|
},
|
||||||
"js_modules": {
|
"js_modules": {
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"https://raw.githubusercontent.com/ntoll/upytest/1.0.9/upytest.py": "",
|
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/upytest.py": "",
|
||||||
"./tests/test_config.py": "tests/test_config.py",
|
"./tests/test_config.py": "tests/test_config.py",
|
||||||
"./tests/test_current_target.py": "tests/test_current_target.py",
|
"./tests/test_current_target.py": "tests/test_current_target.py",
|
||||||
"./tests/test_display.py": "tests/test_display.py",
|
"./tests/test_display.py": "tests/test_display.py",
|
||||||
"./tests/test_document.py": "tests/test_document.py",
|
"./tests/test_document.py": "tests/test_document.py",
|
||||||
"./tests/test_fetch.py": "tests/test_fetch.py",
|
"./tests/test_fetch.py": "tests/test_fetch.py",
|
||||||
"./tests/test_ffi.py": "tests/test_ffi.py",
|
"./tests/test_ffi.py": "tests/test_ffi.py",
|
||||||
"./tests/test_media.py": "tests/test_media.py",
|
|
||||||
"./tests/test_js_modules.py": "tests/test_js_modules.py",
|
"./tests/test_js_modules.py": "tests/test_js_modules.py",
|
||||||
"./tests/test_storage.py": "tests/test_storage.py",
|
"./tests/test_storage.py": "tests/test_storage.py",
|
||||||
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
|
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
|
||||||
"./tests/test_web.py": "tests/test_web.py",
|
"./tests/test_web.py": "tests/test_web.py",
|
||||||
"./tests/test_websocket.py": "tests/test_websocket.py",
|
"./tests/test_websocket.py": "tests/test_websocket.py",
|
||||||
"./tests/test_events.py": "tests/test_events.py",
|
"./tests/test_when.py": "tests/test_when.py",
|
||||||
"./tests/test_window.py": "tests/test_window.py"
|
"./tests/test_window.py": "tests/test_window.py"
|
||||||
},
|
},
|
||||||
"js_modules": {
|
"js_modules": {
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ def test_current_target():
|
|||||||
"""
|
"""
|
||||||
expected = "py-0"
|
expected = "py-0"
|
||||||
if is_micropython:
|
if is_micropython:
|
||||||
expected = "mpy-w0-target" if RUNNING_IN_WORKER else "mpy-0"
|
if RUNNING_IN_WORKER:
|
||||||
|
expected = "mpy-w0-target"
|
||||||
|
else:
|
||||||
|
expected = "mpy-0"
|
||||||
elif RUNNING_IN_WORKER:
|
elif RUNNING_IN_WORKER:
|
||||||
expected = "py-w0-target"
|
expected = "py-w0-target"
|
||||||
assert current_target() == expected, f"Expected {expected} got {current_target()}"
|
assert current_target() == expected, f"Expected {expected} got {current_target()}"
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ async def test_image_display():
|
|||||||
"""
|
"""
|
||||||
Check an image is displayed correctly.
|
Check an image is displayed correctly.
|
||||||
"""
|
"""
|
||||||
_mpl = await py_import("matplotlib")
|
mpl = await py_import("matplotlib")
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
xpoints = [3, 6, 9]
|
xpoints = [3, 6, 9]
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
""""
|
|
||||||
Tests for the PyScript media module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import upytest
|
|
||||||
|
|
||||||
from pyscript import media
|
|
||||||
|
|
||||||
|
|
||||||
async def test_device_enumeration():
|
|
||||||
"""Test enumerating media devices."""
|
|
||||||
devices = await media.list_devices()
|
|
||||||
assert isinstance(devices, list), "list_devices should return a list"
|
|
||||||
|
|
||||||
# If devices are found, verify they have the expected functionality
|
|
||||||
if devices:
|
|
||||||
device = devices[0]
|
|
||||||
|
|
||||||
# Test real device properties exist (but don't assert on their values)
|
|
||||||
# Browser security might restrict actual values until permissions are granted
|
|
||||||
assert hasattr(device, "id"), "Device should have id property"
|
|
||||||
assert hasattr(device, "kind"), "Device should have kind property"
|
|
||||||
assert device.kind in [
|
|
||||||
"videoinput",
|
|
||||||
"audioinput",
|
|
||||||
"audiooutput",
|
|
||||||
], f"Device should have a valid kind, got: {device.kind}"
|
|
||||||
|
|
||||||
# Verify dictionary access works with actual device
|
|
||||||
assert (
|
|
||||||
device["id"] == device.id
|
|
||||||
), "Dictionary access should match property access"
|
|
||||||
assert (
|
|
||||||
device["kind"] == device.kind
|
|
||||||
), "Dictionary access should match property access"
|
|
||||||
|
|
||||||
|
|
||||||
@upytest.skip("Waiting on a bug-fix in MicroPython, for this test to work.", skip_when=upytest.is_micropython)
|
|
||||||
async def test_video_stream_acquisition():
|
|
||||||
"""Test video stream."""
|
|
||||||
try:
|
|
||||||
# Load a video stream
|
|
||||||
stream = await media.Device.load(video=True)
|
|
||||||
|
|
||||||
# Verify we get a real stream with expected properties
|
|
||||||
assert hasattr(stream, "active"), "Stream should have active property"
|
|
||||||
|
|
||||||
# Check for video tracks, but don't fail if permissions aren't granted
|
|
||||||
if stream._dom_element and hasattr(stream._dom_element, "getVideoTracks"):
|
|
||||||
tracks = stream._dom_element.getVideoTracks()
|
|
||||||
if tracks.length > 0:
|
|
||||||
assert True, "Video stream has video tracks"
|
|
||||||
except Exception as e:
|
|
||||||
# If the browser blocks access, the test should still pass
|
|
||||||
# This is because we're testing the API works, not that permissions are granted
|
|
||||||
assert (
|
|
||||||
True
|
|
||||||
), f"Stream acquisition attempted but may require permissions: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
@upytest.skip("Waiting on a bug-fix in MicroPython, for this test to work.", skip_when=upytest.is_micropython)
|
|
||||||
async def test_custom_video_constraints():
|
|
||||||
"""Test loading video with custom constraints."""
|
|
||||||
try:
|
|
||||||
# Define custom constraints
|
|
||||||
constraints = {"width": 640, "height": 480}
|
|
||||||
|
|
||||||
# Load stream with custom constraints
|
|
||||||
stream = await media.Device.load(video=constraints)
|
|
||||||
|
|
||||||
# Basic stream property check
|
|
||||||
assert hasattr(stream, "active"), "Stream should have active property"
|
|
||||||
|
|
||||||
# Check for tracks only if we have access
|
|
||||||
if stream._dom_element and hasattr(stream._dom_element, "getVideoTracks"):
|
|
||||||
tracks = stream._dom_element.getVideoTracks()
|
|
||||||
if tracks.length > 0 and hasattr(tracks[0], "getSettings"):
|
|
||||||
# Settings verification is optional - browsers may handle constraints differently
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
# If the browser blocks access, test that the API structure works
|
|
||||||
assert True, f"Custom constraint test attempted: {str(e)}"
|
|
||||||
@@ -65,6 +65,7 @@ async def test_storage_types():
|
|||||||
assert test_store["string"] == "hello"
|
assert test_store["string"] == "hello"
|
||||||
assert isinstance(test_store["string"], str)
|
assert isinstance(test_store["string"], str)
|
||||||
assert test_store["none"] is None
|
assert test_store["none"] is None
|
||||||
|
assert isinstance(test_store["none"], type(None))
|
||||||
assert test_store["list"] == [1, 2, 3]
|
assert test_store["list"] == [1, 2, 3]
|
||||||
assert isinstance(test_store["list"], list)
|
assert isinstance(test_store["list"], list)
|
||||||
assert test_store["dict"] == {"a": 1, "b": 2}
|
assert test_store["dict"] == {"a": 1, "b": 2}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import upytest
|
|
||||||
import js
|
|
||||||
from pyscript import util
|
|
||||||
|
|
||||||
|
|
||||||
def test_as_bytearray():
|
|
||||||
"""
|
|
||||||
Test the as_bytearray function correctly converts a JavaScript ArrayBuffer
|
|
||||||
to a Python bytearray.
|
|
||||||
"""
|
|
||||||
msg = b"Hello, world!"
|
|
||||||
buffer = js.ArrayBuffer.new(len(msg))
|
|
||||||
ui8a = js.Uint8Array.new(buffer)
|
|
||||||
for b in msg:
|
|
||||||
ui8a[i] = b
|
|
||||||
ba = util.as_bytearray(buffer)
|
|
||||||
assert isinstance(ba, bytearray)
|
|
||||||
assert ba == msg
|
|
||||||
|
|
||||||
|
|
||||||
def test_not_supported():
|
|
||||||
"""
|
|
||||||
Test the NotSupported class raises an exception when trying to access
|
|
||||||
attributes or call the object.
|
|
||||||
"""
|
|
||||||
ns = util.NotSupported("test", "This is not supported.")
|
|
||||||
with upytest.raises(AttributeError) as e:
|
|
||||||
ns.test
|
|
||||||
assert str(e.exception) == "This is not supported.", str(e.exception)
|
|
||||||
with upytest.raises(AttributeError) as e:
|
|
||||||
ns.test = 1
|
|
||||||
assert str(e.exception) == "This is not supported.", str(e.exception)
|
|
||||||
with upytest.raises(TypeError) as e:
|
|
||||||
ns()
|
|
||||||
assert str(e.exception) == "This is not supported.", str(e.exception)
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_awaitable():
|
|
||||||
"""
|
|
||||||
Test the is_awaitable function correctly identifies an asynchronous
|
|
||||||
function.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def async_func():
|
|
||||||
yield
|
|
||||||
|
|
||||||
assert util.is_awaitable(async_func)
|
|
||||||
assert not util.is_awaitable(lambda: None)
|
|
||||||
@@ -164,57 +164,6 @@ class TestElement:
|
|||||||
await call_flag.wait()
|
await call_flag.wait()
|
||||||
assert called
|
assert called
|
||||||
|
|
||||||
async def test_when_decorator_on_event(self):
|
|
||||||
called = False
|
|
||||||
|
|
||||||
another_button = web.page.find("#another-test-button")[0]
|
|
||||||
call_flag = asyncio.Event()
|
|
||||||
|
|
||||||
assert another_button.on_click is not None
|
|
||||||
assert isinstance(another_button.on_click, web.Event)
|
|
||||||
|
|
||||||
@when(another_button.on_click)
|
|
||||||
def on_click(event):
|
|
||||||
nonlocal called
|
|
||||||
called = True
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
# Now let's simulate a click on the button (using the low level JS API)
|
|
||||||
# so we don't risk dom getting in the way
|
|
||||||
assert not called
|
|
||||||
another_button._dom_element.click()
|
|
||||||
await call_flag.wait()
|
|
||||||
assert called
|
|
||||||
|
|
||||||
async def test_on_event_with_default_handler(self):
|
|
||||||
called = False
|
|
||||||
call_flag = asyncio.Event()
|
|
||||||
|
|
||||||
def handler(event):
|
|
||||||
nonlocal called
|
|
||||||
called = True
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
b = web.button("Click me", on_click=handler)
|
|
||||||
|
|
||||||
# Now let's simulate a click on the button (using the low level JS API)
|
|
||||||
# so we don't risk dom getting in the way
|
|
||||||
assert not called
|
|
||||||
b._dom_element.click()
|
|
||||||
await call_flag.wait()
|
|
||||||
assert called
|
|
||||||
|
|
||||||
def test_on_event_must_be_actual_event(self):
|
|
||||||
"""
|
|
||||||
Any on_FOO event must relate to an actual FOO event on the element.
|
|
||||||
"""
|
|
||||||
b = web.button("Click me")
|
|
||||||
# Non-existent event causes a ValueError
|
|
||||||
with upytest.raises(ValueError):
|
|
||||||
b.on_chicken
|
|
||||||
# Buttons have an underlying "click" event so this will work.
|
|
||||||
assert b.on_click
|
|
||||||
|
|
||||||
def test_inner_html_attribute(self):
|
def test_inner_html_attribute(self):
|
||||||
# GIVEN an existing element on the page with a known empty text content
|
# GIVEN an existing element on the page with a known empty text content
|
||||||
div = web.page.find("#element_attribute_tests")[0]
|
div = web.page.find("#element_attribute_tests")[0]
|
||||||
@@ -248,7 +197,7 @@ class TestCollection:
|
|||||||
|
|
||||||
def test_iter_eq_children(self):
|
def test_iter_eq_children(self):
|
||||||
elements = web.page.find(".multi-elems")
|
elements = web.page.find(".multi-elems")
|
||||||
assert list(elements) == list(elements.elements)
|
assert [el for el in elements] == [el for el in elements.elements]
|
||||||
assert len(elements) == 3
|
assert len(elements) == 3
|
||||||
|
|
||||||
def test_slices(self):
|
def test_slices(self):
|
||||||
@@ -278,15 +227,11 @@ class TestCollection:
|
|||||||
assert el.style["background-color"] != "red"
|
assert el.style["background-color"] != "red"
|
||||||
assert elements[i].style["background-color"] != "red"
|
assert elements[i].style["background-color"] != "red"
|
||||||
|
|
||||||
@upytest.skip(
|
|
||||||
"Flakey in Pyodide on Worker",
|
|
||||||
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
|
|
||||||
)
|
|
||||||
async def test_when_decorator(self):
|
async def test_when_decorator(self):
|
||||||
called = False
|
called = False
|
||||||
call_flag = asyncio.Event()
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
buttons_collection = web.page["button"]
|
buttons_collection = web.page.find("button")
|
||||||
|
|
||||||
@when("click", buttons_collection)
|
@when("click", buttons_collection)
|
||||||
def on_click(event):
|
def on_click(event):
|
||||||
@@ -304,28 +249,6 @@ class TestCollection:
|
|||||||
called = False
|
called = False
|
||||||
call_flag.clear()
|
call_flag.clear()
|
||||||
|
|
||||||
async def test_when_decorator_on_event(self):
|
|
||||||
call_counter = 0
|
|
||||||
call_flag = asyncio.Event()
|
|
||||||
|
|
||||||
buttons_collection = web.page.find("button")
|
|
||||||
number_of_clicks = len(buttons_collection)
|
|
||||||
|
|
||||||
@when(buttons_collection.on_click)
|
|
||||||
def on_click(event):
|
|
||||||
nonlocal call_counter
|
|
||||||
call_counter += 1
|
|
||||||
if call_counter == number_of_clicks:
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
# Now let's simulate a click on the button (using the low level JS API)
|
|
||||||
# so we don't risk dom getting in the way
|
|
||||||
assert call_counter == 0
|
|
||||||
for button in buttons_collection:
|
|
||||||
button._dom_element.click()
|
|
||||||
await call_flag.wait()
|
|
||||||
assert call_counter == number_of_clicks
|
|
||||||
|
|
||||||
|
|
||||||
class TestCreation:
|
class TestCreation:
|
||||||
|
|
||||||
@@ -427,18 +350,18 @@ class TestInput:
|
|||||||
class TestSelect:
|
class TestSelect:
|
||||||
|
|
||||||
def test_select_options_iter(self):
|
def test_select_options_iter(self):
|
||||||
select = web.page.find("#test_select_element_w_options")[0]
|
select = web.page.find(f"#test_select_element_w_options")[0]
|
||||||
|
|
||||||
for i, option in enumerate(select.options, 1):
|
for i, option in enumerate(select.options, 1):
|
||||||
assert option.value == f"{i}"
|
assert option.value == f"{i}"
|
||||||
assert option.innerHTML == f"Option {i}"
|
assert option.innerHTML == f"Option {i}"
|
||||||
|
|
||||||
def test_select_options_len(self):
|
def test_select_options_len(self):
|
||||||
select = web.page.find("#test_select_element_w_options")[0]
|
select = web.page.find(f"#test_select_element_w_options")[0]
|
||||||
assert len(select.options) == 2
|
assert len(select.options) == 2
|
||||||
|
|
||||||
def test_select_options_clear(self):
|
def test_select_options_clear(self):
|
||||||
select = web.page.find("#test_select_element_to_clear")[0]
|
select = web.page.find(f"#test_select_element_to_clear")[0]
|
||||||
assert len(select.options) == 3
|
assert len(select.options) == 3
|
||||||
|
|
||||||
select.options.clear()
|
select.options.clear()
|
||||||
@@ -447,7 +370,7 @@ class TestSelect:
|
|||||||
|
|
||||||
def test_select_element_add(self):
|
def test_select_element_add(self):
|
||||||
# GIVEN the existing select element with no options
|
# GIVEN the existing select element with no options
|
||||||
select = web.page.find("#test_select_element")[0]
|
select = web.page.find(f"#test_select_element")[0]
|
||||||
|
|
||||||
# EXPECT the select element to have no options
|
# EXPECT the select element to have no options
|
||||||
assert len(select.options) == 0
|
assert len(select.options) == 0
|
||||||
@@ -498,14 +421,20 @@ class TestSelect:
|
|||||||
# EXPECT the middle option to have the value and html we passed in
|
# EXPECT the middle option to have the value and html we passed in
|
||||||
assert select.options[0].value == "1"
|
assert select.options[0].value == "1"
|
||||||
assert select.options[0].innerHTML == "Option 1"
|
assert select.options[0].innerHTML == "Option 1"
|
||||||
assert select.options[0].selected == select.options[0]._dom_element.selected
|
assert (
|
||||||
assert select.options[0].selected is False
|
select.options[0].selected
|
||||||
|
== select.options[0]._dom_element.selected
|
||||||
|
== False
|
||||||
|
)
|
||||||
assert select.options[1].value == "2"
|
assert select.options[1].value == "2"
|
||||||
assert select.options[1].innerHTML == "Option 2"
|
assert select.options[1].innerHTML == "Option 2"
|
||||||
assert select.options[2].value == "3"
|
assert select.options[2].value == "3"
|
||||||
assert select.options[2].innerHTML == "Option 3"
|
assert select.options[2].innerHTML == "Option 3"
|
||||||
assert select.options[2].selected == select.options[2]._dom_element.selected
|
assert (
|
||||||
assert select.options[2].selected is True
|
select.options[2].selected
|
||||||
|
== select.options[2]._dom_element.selected
|
||||||
|
== True
|
||||||
|
)
|
||||||
assert select.options[3].value == ""
|
assert select.options[3].value == ""
|
||||||
assert select.options[3].innerHTML == ""
|
assert select.options[3].innerHTML == ""
|
||||||
|
|
||||||
@@ -532,7 +461,7 @@ class TestSelect:
|
|||||||
|
|
||||||
def test_select_options_remove(self):
|
def test_select_options_remove(self):
|
||||||
# GIVEN the existing select element with 3 options
|
# GIVEN the existing select element with 3 options
|
||||||
select = web.page.find("#test_select_element_to_remove")[0]
|
select = web.page.find(f"#test_select_element_to_remove")[0]
|
||||||
|
|
||||||
# EXPECT the select element to have 3 options
|
# EXPECT the select element to have 3 options
|
||||||
assert len(select.options) == 4
|
assert len(select.options) == 4
|
||||||
@@ -554,7 +483,7 @@ class TestSelect:
|
|||||||
|
|
||||||
def test_select_get_selected_option(self):
|
def test_select_get_selected_option(self):
|
||||||
# GIVEN the existing select element with one selected option
|
# GIVEN the existing select element with one selected option
|
||||||
select = web.page.find("#test_select_element_w_options")[0]
|
select = web.page.find(f"#test_select_element_w_options")[0]
|
||||||
|
|
||||||
# WHEN we get the selected option
|
# WHEN we get the selected option
|
||||||
selected_option = select.options.selected
|
selected_option = select.options.selected
|
||||||
@@ -562,8 +491,7 @@ class TestSelect:
|
|||||||
# EXPECT the selected option to be correct
|
# EXPECT the selected option to be correct
|
||||||
assert selected_option.value == "2"
|
assert selected_option.value == "2"
|
||||||
assert selected_option.innerHTML == "Option 2"
|
assert selected_option.innerHTML == "Option 2"
|
||||||
assert selected_option.selected == selected_option._dom_element.selected
|
assert selected_option.selected == selected_option._dom_element.selected == True
|
||||||
assert selected_option.selected is True
|
|
||||||
|
|
||||||
|
|
||||||
class TestElements:
|
class TestElements:
|
||||||
@@ -620,8 +548,7 @@ class TestElements:
|
|||||||
el = klass(*args, **kwargs)
|
el = klass(*args, **kwargs)
|
||||||
container.append(el)
|
container.append(el)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = f"Failed to create element {el_type}: {e}"
|
assert False, f"Failed to create element {el_type}: {e}"
|
||||||
raise AssertionError(msg)
|
|
||||||
|
|
||||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||||
# check the return tag from the selector
|
# check the return tag from the selector
|
||||||
@@ -832,13 +759,14 @@ class TestElements:
|
|||||||
self._create_el_and_basic_asserts("iframe", properties=properties)
|
self._create_el_and_basic_asserts("iframe", properties=properties)
|
||||||
|
|
||||||
@upytest.skip(
|
@upytest.skip(
|
||||||
"Flakey in worker.",
|
"Flakey on Pyodide in worker.",
|
||||||
skip_when=RUNNING_IN_WORKER,
|
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
|
||||||
)
|
)
|
||||||
async def test_img(self):
|
async def test_img(self):
|
||||||
"""
|
"""
|
||||||
This test, thanks to downloading an image from the internet, is flakey
|
This test contains a bespoke version of the _create_el_and_basic_asserts
|
||||||
when run in a worker. It's skipped when running in a worker.
|
function so we can await asyncio.sleep if in a worker, so the DOM state
|
||||||
|
is in sync with the worker before property based asserts can happen.
|
||||||
"""
|
"""
|
||||||
properties = {
|
properties = {
|
||||||
"src": "https://picsum.photos/600/400",
|
"src": "https://picsum.photos/600/400",
|
||||||
@@ -846,7 +774,39 @@ class TestElements:
|
|||||||
"width": 250,
|
"width": 250,
|
||||||
"height": 200,
|
"height": 200,
|
||||||
}
|
}
|
||||||
self._create_el_and_basic_asserts("img", properties=properties)
|
|
||||||
|
def parse_value(v):
|
||||||
|
if isinstance(v, bool):
|
||||||
|
return str(v)
|
||||||
|
|
||||||
|
return f"{v}"
|
||||||
|
|
||||||
|
args = []
|
||||||
|
kwargs = {}
|
||||||
|
|
||||||
|
if properties:
|
||||||
|
kwargs = {k: parse_value(v) for k, v in properties.items()}
|
||||||
|
|
||||||
|
# Let's make sure the target div to contain the element is empty.
|
||||||
|
container = web.page["#test-element-container"][0]
|
||||||
|
container.innerHTML = ""
|
||||||
|
assert container.innerHTML == "", container.innerHTML
|
||||||
|
# Let's create the element
|
||||||
|
try:
|
||||||
|
klass = getattr(web, "img")
|
||||||
|
el = klass(*args, **kwargs)
|
||||||
|
container.append(el)
|
||||||
|
except Exception as e:
|
||||||
|
assert False, f"Failed to create element img: {e}"
|
||||||
|
|
||||||
|
if RUNNING_IN_WORKER:
|
||||||
|
# Needed to sync the DOM with the worker.
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# Check the img element was created correctly and all its properties
|
||||||
|
# were set correctly.
|
||||||
|
for k, v in properties.items():
|
||||||
|
assert v == getattr(el, k), f"{k} should be {v} but is {getattr(el, k)}"
|
||||||
|
|
||||||
def test_input(self):
|
def test_input(self):
|
||||||
# TODO: we need multiple input tests
|
# TODO: we need multiple input tests
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ Exercise the pyscript.Websocket class.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import upytest
|
|
||||||
|
|
||||||
from pyscript import WebSocket
|
from pyscript import WebSocket
|
||||||
|
|
||||||
|
|
||||||
@upytest.skip("Websocket tests are disabled.")
|
|
||||||
async def test_websocket_with_attributes():
|
async def test_websocket_with_attributes():
|
||||||
"""
|
"""
|
||||||
Event handlers assigned via object attributes.
|
Event handlers assigned via object attributes.
|
||||||
@@ -54,7 +52,6 @@ async def test_websocket_with_attributes():
|
|||||||
assert closed_flag is True
|
assert closed_flag is True
|
||||||
|
|
||||||
|
|
||||||
@upytest.skip("Websocket tests are disabled.")
|
|
||||||
async def test_websocket_with_init():
|
async def test_websocket_with_init():
|
||||||
"""
|
"""
|
||||||
Event handlers assigned via __init__ arguments.
|
Event handlers assigned via __init__ arguments.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Tests for the when function and Event class.
|
Tests for the pyscript.when decorator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import upytest
|
import upytest
|
||||||
from pyscript import RUNNING_IN_WORKER, web, Event, when
|
from pyscript import RUNNING_IN_WORKER, web
|
||||||
|
|
||||||
|
|
||||||
def get_container():
|
def get_container():
|
||||||
@@ -22,96 +22,10 @@ def teardown():
|
|||||||
container.innerHTML = ""
|
container.innerHTML = ""
|
||||||
|
|
||||||
|
|
||||||
def test_event_add_listener():
|
|
||||||
"""
|
|
||||||
Adding a listener to an event should add it to the list of listeners. It
|
|
||||||
should only be added once.
|
|
||||||
"""
|
|
||||||
event = Event()
|
|
||||||
listener = lambda x: x
|
|
||||||
event.add_listener(listener)
|
|
||||||
event.add_listener(listener)
|
|
||||||
assert len(event._listeners) == 1 # Only one item added.
|
|
||||||
assert listener in event._listeners # The item is the expected listener.
|
|
||||||
|
|
||||||
|
|
||||||
def test_event_remove_listener():
|
|
||||||
"""
|
|
||||||
Removing a listener from an event should remove it from the list of
|
|
||||||
listeners.
|
|
||||||
"""
|
|
||||||
event = Event()
|
|
||||||
listener1 = lambda x: x
|
|
||||||
listener2 = lambda x: x
|
|
||||||
event.add_listener(listener1)
|
|
||||||
event.add_listener(listener2)
|
|
||||||
assert len(event._listeners) == 2 # Two listeners added.
|
|
||||||
assert listener1 in event._listeners # The first listener is in the list.
|
|
||||||
assert listener2 in event._listeners # The second listener is in the list.
|
|
||||||
event.remove_listener(listener1)
|
|
||||||
assert len(event._listeners) == 1 # Only one item remains.
|
|
||||||
assert listener2 in event._listeners # The second listener is in the list.
|
|
||||||
|
|
||||||
|
|
||||||
def test_event_remove_all_listeners():
|
|
||||||
"""
|
|
||||||
Removing all listeners from an event should clear the list of listeners.
|
|
||||||
"""
|
|
||||||
event = Event()
|
|
||||||
listener1 = lambda x: x
|
|
||||||
listener2 = lambda x: x
|
|
||||||
event.add_listener(listener1)
|
|
||||||
event.add_listener(listener2)
|
|
||||||
assert len(event._listeners) == 2 # Two listeners added.
|
|
||||||
event.remove_listener()
|
|
||||||
assert len(event._listeners) == 0 # No listeners remain.
|
|
||||||
|
|
||||||
|
|
||||||
def test_event_trigger():
|
|
||||||
"""
|
|
||||||
Triggering an event should call all of the listeners with the provided
|
|
||||||
arguments.
|
|
||||||
"""
|
|
||||||
event = Event()
|
|
||||||
counter = 0
|
|
||||||
|
|
||||||
def listener(x):
|
|
||||||
nonlocal counter
|
|
||||||
counter += 1
|
|
||||||
assert x == "ok"
|
|
||||||
|
|
||||||
event.add_listener(listener)
|
|
||||||
assert counter == 0 # The listener has not been triggered yet.
|
|
||||||
event.trigger("ok")
|
|
||||||
assert counter == 1 # The listener has been triggered with the expected result.
|
|
||||||
|
|
||||||
|
|
||||||
async def test_event_trigger_with_awaitable():
|
|
||||||
"""
|
|
||||||
Triggering an event with an awaitable listener should call the listener
|
|
||||||
with the provided arguments.
|
|
||||||
"""
|
|
||||||
call_flag = asyncio.Event()
|
|
||||||
event = Event()
|
|
||||||
counter = 0
|
|
||||||
|
|
||||||
async def listener(x):
|
|
||||||
nonlocal counter
|
|
||||||
counter += 1
|
|
||||||
assert x == "ok"
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
event.add_listener(listener)
|
|
||||||
assert counter == 0 # The listener has not been triggered yet.
|
|
||||||
event.trigger("ok")
|
|
||||||
await call_flag.wait()
|
|
||||||
assert counter == 1 # The listener has been triggered with the expected result.
|
|
||||||
|
|
||||||
|
|
||||||
async def test_when_decorator_with_event():
|
async def test_when_decorator_with_event():
|
||||||
"""
|
"""
|
||||||
When the decorated function takes a single parameter,
|
When the decorated function takes a single parameter,
|
||||||
it should be passed the event object.
|
it should be passed the event object
|
||||||
"""
|
"""
|
||||||
btn = web.button("foo_button", id="foo_id")
|
btn = web.button("foo_button", id="foo_id")
|
||||||
container = get_container()
|
container = get_container()
|
||||||
@@ -120,7 +34,7 @@ async def test_when_decorator_with_event():
|
|||||||
called = False
|
called = False
|
||||||
call_flag = asyncio.Event()
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
@when("click", selector="#foo_id")
|
@web.when("click", selector="#foo_id")
|
||||||
def foo(evt):
|
def foo(evt):
|
||||||
nonlocal called
|
nonlocal called
|
||||||
called = evt
|
called = evt
|
||||||
@@ -134,7 +48,7 @@ async def test_when_decorator_with_event():
|
|||||||
async def test_when_decorator_without_event():
|
async def test_when_decorator_without_event():
|
||||||
"""
|
"""
|
||||||
When the decorated function takes no parameters (not including 'self'),
|
When the decorated function takes no parameters (not including 'self'),
|
||||||
it should be called without the event object.
|
it should be called without the event object
|
||||||
"""
|
"""
|
||||||
btn = web.button("foo_button", id="foo_id")
|
btn = web.button("foo_button", id="foo_id")
|
||||||
container = get_container()
|
container = get_container()
|
||||||
@@ -151,53 +65,7 @@ async def test_when_decorator_without_event():
|
|||||||
|
|
||||||
btn.click()
|
btn.click()
|
||||||
await call_flag.wait()
|
await call_flag.wait()
|
||||||
assert called is True
|
assert called
|
||||||
|
|
||||||
|
|
||||||
async def test_when_decorator_with_event_as_async_handler():
|
|
||||||
"""
|
|
||||||
When the decorated function takes a single parameter,
|
|
||||||
it should be passed the event object. Async version.
|
|
||||||
"""
|
|
||||||
btn = web.button("foo_button", id="foo_id")
|
|
||||||
container = get_container()
|
|
||||||
container.append(btn)
|
|
||||||
|
|
||||||
called = False
|
|
||||||
call_flag = asyncio.Event()
|
|
||||||
|
|
||||||
@when("click", selector="#foo_id")
|
|
||||||
async def foo(evt):
|
|
||||||
nonlocal called
|
|
||||||
called = evt
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
btn.click()
|
|
||||||
await call_flag.wait()
|
|
||||||
assert called.target.id == "foo_id"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_when_decorator_without_event_as_async_handler():
|
|
||||||
"""
|
|
||||||
When the decorated function takes no parameters (not including 'self'),
|
|
||||||
it should be called without the event object. Async version.
|
|
||||||
"""
|
|
||||||
btn = web.button("foo_button", id="foo_id")
|
|
||||||
container = get_container()
|
|
||||||
container.append(btn)
|
|
||||||
|
|
||||||
called = False
|
|
||||||
call_flag = asyncio.Event()
|
|
||||||
|
|
||||||
@web.when("click", selector="#foo_id")
|
|
||||||
async def foo():
|
|
||||||
nonlocal called
|
|
||||||
called = True
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
btn.click()
|
|
||||||
await call_flag.wait()
|
|
||||||
assert called is True
|
|
||||||
|
|
||||||
|
|
||||||
async def test_two_when_decorators():
|
async def test_two_when_decorators():
|
||||||
@@ -213,13 +81,13 @@ async def test_two_when_decorators():
|
|||||||
call_flag1 = asyncio.Event()
|
call_flag1 = asyncio.Event()
|
||||||
call_flag2 = asyncio.Event()
|
call_flag2 = asyncio.Event()
|
||||||
|
|
||||||
@when("click", selector="#foo_id")
|
@web.when("click", selector="#foo_id")
|
||||||
def foo1(evt):
|
def foo1(evt):
|
||||||
nonlocal called1
|
nonlocal called1
|
||||||
called1 = True
|
called1 = True
|
||||||
call_flag1.set()
|
call_flag1.set()
|
||||||
|
|
||||||
@when("click", selector="#foo_id")
|
@web.when("click", selector="#foo_id")
|
||||||
def foo2(evt):
|
def foo2(evt):
|
||||||
nonlocal called2
|
nonlocal called2
|
||||||
called2 = True
|
called2 = True
|
||||||
@@ -232,6 +100,31 @@ async def test_two_when_decorators():
|
|||||||
assert called2
|
assert called2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_two_when_decorators_same_element():
|
||||||
|
"""
|
||||||
|
When decorating a function twice *on the same DOM element*, both should
|
||||||
|
function
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
@web.when("click", selector="#foo_id")
|
||||||
|
@web.when("click", selector="#foo_id")
|
||||||
|
def foo(evt):
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
assert counter == 0, counter
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert counter == 2, counter
|
||||||
|
|
||||||
|
|
||||||
async def test_when_decorator_multiple_elements():
|
async def test_when_decorator_multiple_elements():
|
||||||
"""
|
"""
|
||||||
The @when decorator's selector should successfully select multiple
|
The @when decorator's selector should successfully select multiple
|
||||||
@@ -259,7 +152,7 @@ async def test_when_decorator_multiple_elements():
|
|||||||
call_flag1 = asyncio.Event()
|
call_flag1 = asyncio.Event()
|
||||||
call_flag2 = asyncio.Event()
|
call_flag2 = asyncio.Event()
|
||||||
|
|
||||||
@when("click", selector=".foo_class")
|
@web.when("click", selector=".foo_class")
|
||||||
def foo(evt):
|
def foo(evt):
|
||||||
nonlocal counter
|
nonlocal counter
|
||||||
counter += 1
|
counter += 1
|
||||||
@@ -277,6 +170,31 @@ async def test_when_decorator_multiple_elements():
|
|||||||
assert counter == 2, counter
|
assert counter == 2, counter
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_duplicate_selectors():
|
||||||
|
"""
|
||||||
|
When is not idempotent, so it should be possible to add multiple
|
||||||
|
@when decorators with the same selector.
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
@web.when("click", selector="#foo_id")
|
||||||
|
@web.when("click", selector="#foo_id") # duplicate
|
||||||
|
def foo1(evt):
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
assert counter == 0, counter
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert counter == 2, counter
|
||||||
|
|
||||||
|
|
||||||
@upytest.skip(
|
@upytest.skip(
|
||||||
"Only works in Pyodide on main thread",
|
"Only works in Pyodide on main thread",
|
||||||
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
|
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
|
||||||
@@ -292,24 +210,55 @@ def test_when_decorator_invalid_selector():
|
|||||||
|
|
||||||
with upytest.raises(JsException) as e:
|
with upytest.raises(JsException) as e:
|
||||||
|
|
||||||
@when("click", selector="#.bad")
|
@web.when("click", selector="#.bad")
|
||||||
def foo(evt): ...
|
def foo(evt): ...
|
||||||
|
|
||||||
assert "'#.bad' is not a valid selector" in str(e.exception), str(e.exception)
|
assert "'#.bad' is not a valid selector" in str(e.exception), str(e.exception)
|
||||||
|
|
||||||
|
|
||||||
def test_when_decorates_an_event():
|
def test_when_decorates_a_whenable():
|
||||||
"""
|
"""
|
||||||
When the @when decorator is used on a function to handle an Event instance,
|
When the @when decorator is used on a function to handle a whenable object,
|
||||||
the function should be called when the Event object is triggered.
|
the function should be called when the whenable object is triggered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
whenable = Event()
|
class MyWhenable:
|
||||||
|
"""
|
||||||
|
A simple whenable object that can be triggered.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.handler = None
|
||||||
|
self.args = None
|
||||||
|
self.kwargs = None
|
||||||
|
|
||||||
|
def trigger(self):
|
||||||
|
"""
|
||||||
|
Triggers the whenable object, resulting in the handler being
|
||||||
|
called.
|
||||||
|
"""
|
||||||
|
if self.handler:
|
||||||
|
result = {
|
||||||
|
"args": self.args,
|
||||||
|
"kwargs": self.kwargs,
|
||||||
|
}
|
||||||
|
self.handler(result) # call the handler
|
||||||
|
|
||||||
|
def __when__(self, handler, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
These implementation details depend on the sort of thing the
|
||||||
|
whenable object represents. This is just a simple example.
|
||||||
|
"""
|
||||||
|
self.handler = handler
|
||||||
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
whenable = MyWhenable()
|
||||||
counter = 0
|
counter = 0
|
||||||
|
|
||||||
# When as a decorator.
|
# When as a decorator.
|
||||||
@when(whenable)
|
@web.when(whenable, "foo", "bar", baz="qux")
|
||||||
def handler(result):
|
def foo(result):
|
||||||
"""
|
"""
|
||||||
A function that should be called when the whenable object is triggered.
|
A function that should be called when the whenable object is triggered.
|
||||||
|
|
||||||
@@ -318,23 +267,56 @@ def test_when_decorates_an_event():
|
|||||||
"""
|
"""
|
||||||
nonlocal counter
|
nonlocal counter
|
||||||
counter += 1
|
counter += 1
|
||||||
assert result == "ok"
|
assert result["args"] == ("foo", "bar")
|
||||||
|
assert result["kwargs"] == {"baz": "qux"}
|
||||||
|
|
||||||
# The function should not be called until the whenable object is triggered.
|
# The function should not be called until the whenable object is triggered.
|
||||||
assert counter == 0
|
assert counter == 0
|
||||||
# Trigger the whenable object.
|
# Trigger the whenable object.
|
||||||
whenable.trigger("ok")
|
whenable.trigger()
|
||||||
# The function should have been called when the whenable object was
|
# The function should have been called when the whenable object was
|
||||||
# triggered.
|
# triggered.
|
||||||
assert counter == 1
|
assert counter == 1
|
||||||
|
|
||||||
|
|
||||||
def test_when_called_with_an_event_and_handler():
|
def test_when_called_with_a_whenable():
|
||||||
"""
|
"""
|
||||||
The when function should be able to be called with an Event object,
|
The when function should be able to be called with a whenable object,
|
||||||
and a handler function.
|
a handler function, and arguments.
|
||||||
"""
|
"""
|
||||||
whenable = Event()
|
|
||||||
|
class MyWhenable:
|
||||||
|
"""
|
||||||
|
A simple whenable object that can be triggered.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.handler = None
|
||||||
|
self.args = None
|
||||||
|
self.kwargs = None
|
||||||
|
|
||||||
|
def trigger(self):
|
||||||
|
"""
|
||||||
|
Triggers the whenable object, resulting in the handler being
|
||||||
|
called.
|
||||||
|
"""
|
||||||
|
if self.handler:
|
||||||
|
result = {
|
||||||
|
"args": self.args,
|
||||||
|
"kwargs": self.kwargs,
|
||||||
|
}
|
||||||
|
self.handler(result) # call the handler
|
||||||
|
|
||||||
|
def __when__(self, handler, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
These implementation details depend on the sort of thing the
|
||||||
|
whenable object represents. This is just a simple example.
|
||||||
|
"""
|
||||||
|
self.handler = handler
|
||||||
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
whenable = MyWhenable()
|
||||||
counter = 0
|
counter = 0
|
||||||
|
|
||||||
def handler(result):
|
def handler(result):
|
||||||
@@ -346,15 +328,16 @@ def test_when_called_with_an_event_and_handler():
|
|||||||
"""
|
"""
|
||||||
nonlocal counter
|
nonlocal counter
|
||||||
counter += 1
|
counter += 1
|
||||||
assert result == "ok"
|
assert result["args"] == ("foo", "bar")
|
||||||
|
assert result["kwargs"] == {"baz": "qux"}
|
||||||
|
|
||||||
# When as a function.
|
# When as a function.
|
||||||
when(whenable, handler)
|
web.when(whenable, handler, "foo", "bar", baz="qux")
|
||||||
|
|
||||||
# The function should not be called until the whenable object is triggered.
|
# The function should not be called until the whenable object is triggered.
|
||||||
assert counter == 0
|
assert counter == 0
|
||||||
# Trigger the whenable object.
|
# Trigger the whenable object.
|
||||||
whenable.trigger("ok")
|
whenable.trigger()
|
||||||
# The function should have been called when the whenable object was
|
# The function should have been called when the whenable object was
|
||||||
# triggered.
|
# triggered.
|
||||||
assert counter == 1
|
assert counter == 1
|
||||||
18
core/types/3rd-party/xterm-readline.d.ts
vendored
18
core/types/3rd-party/xterm-readline.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
declare var v: any;
|
declare var b: any;
|
||||||
declare var k: boolean;
|
declare var I: boolean;
|
||||||
declare namespace i {
|
declare namespace r {
|
||||||
export let __esModule: boolean;
|
export let __esModule: boolean;
|
||||||
export { Readline };
|
export { Readline };
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ declare class Readline {
|
|||||||
highlighter: any;
|
highlighter: any;
|
||||||
history: any;
|
history: any;
|
||||||
promptSize: any;
|
promptSize: any;
|
||||||
layout: c;
|
layout: p;
|
||||||
buffer(): string;
|
buffer(): string;
|
||||||
shouldHighlight(): boolean;
|
shouldHighlight(): boolean;
|
||||||
clearScreen(): void;
|
clearScreen(): void;
|
||||||
@@ -124,15 +124,15 @@ declare class Readline {
|
|||||||
readPaste(t: any): void;
|
readPaste(t: any): void;
|
||||||
readKey(t: any): void;
|
readKey(t: any): void;
|
||||||
}
|
}
|
||||||
declare class c {
|
declare class p {
|
||||||
constructor(t: any);
|
constructor(t: any);
|
||||||
promptSize: any;
|
promptSize: any;
|
||||||
cursor: u;
|
cursor: c;
|
||||||
end: u;
|
end: c;
|
||||||
}
|
}
|
||||||
declare class u {
|
declare class c {
|
||||||
constructor(t: any, e: any);
|
constructor(t: any, e: any);
|
||||||
row: any;
|
row: any;
|
||||||
col: any;
|
col: any;
|
||||||
}
|
}
|
||||||
export { v as Readline, k as __esModule, i as default };
|
export { b as Readline, I as __esModule, r as default };
|
||||||
|
|||||||
8
core/types/3rd-party/xterm.d.ts
vendored
8
core/types/3rd-party/xterm.d.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
declare var D: any;
|
declare var i: any;
|
||||||
declare var R: any;
|
declare var s: any;
|
||||||
declare var L: {};
|
declare var t: {};
|
||||||
export { D as Terminal, R as __esModule, L as default };
|
export { i as Terminal, s as __esModule, t as default };
|
||||||
|
|||||||
5
core/types/config.d.ts
vendored
5
core/types/config.d.ts
vendored
@@ -1,7 +1,2 @@
|
|||||||
export function configDetails(config: string, type: string | null): {
|
|
||||||
json: boolean;
|
|
||||||
toml: boolean;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
export const configs: Map<any, any>;
|
export const configs: Map<any, any>;
|
||||||
export function relative_url(url: any, base?: string): string;
|
export function relative_url(url: any, base?: string): string;
|
||||||
|
|||||||
3
core/types/core.d.ts
vendored
3
core/types/core.d.ts
vendored
@@ -7,7 +7,6 @@ export function donkey(options: any): Promise<{
|
|||||||
kill: () => void;
|
kill: () => void;
|
||||||
}>;
|
}>;
|
||||||
export function offline_interpreter(config: any): string;
|
export function offline_interpreter(config: any): string;
|
||||||
import codemirror from "./plugins/codemirror.js";
|
|
||||||
import { stdlib } from "./stdlib.js";
|
import { stdlib } from "./stdlib.js";
|
||||||
import { optional } from "./stdlib.js";
|
import { optional } from "./stdlib.js";
|
||||||
import { inputFailure } from "./hooks.js";
|
import { inputFailure } from "./hooks.js";
|
||||||
@@ -64,4 +63,4 @@ declare const exportedHooks: {
|
|||||||
};
|
};
|
||||||
declare const exportedConfig: {};
|
declare const exportedConfig: {};
|
||||||
declare const exportedWhenDefined: any;
|
declare const exportedWhenDefined: any;
|
||||||
export { codemirror, stdlib, optional, inputFailure, TYPES, relative_url, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
export { stdlib, optional, inputFailure, TYPES, relative_url, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
||||||
|
|||||||
8
core/types/fs.d.ts
vendored
8
core/types/fs.d.ts
vendored
@@ -1,8 +0,0 @@
|
|||||||
export const NAMESPACE: "@pyscript.fs";
|
|
||||||
export const ERROR: "storage permissions not granted";
|
|
||||||
export const idb: any;
|
|
||||||
export function getFileSystemDirectoryHandle(options: {
|
|
||||||
id?: string;
|
|
||||||
mode?: "read" | "readwrite";
|
|
||||||
hint?: "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
|
|
||||||
}): Promise<FileSystemDirectoryHandle>;
|
|
||||||
2
core/types/plugins.d.ts
vendored
2
core/types/plugins.d.ts
vendored
@@ -1,10 +1,8 @@
|
|||||||
declare const _default: {
|
declare const _default: {
|
||||||
codemirror: () => Promise<typeof import("./plugins/codemirror.js")>;
|
|
||||||
"deprecations-manager": () => Promise<typeof import("./plugins/deprecations-manager.js")>;
|
"deprecations-manager": () => Promise<typeof import("./plugins/deprecations-manager.js")>;
|
||||||
donkey: () => Promise<typeof import("./plugins/donkey.js")>;
|
donkey: () => Promise<typeof import("./plugins/donkey.js")>;
|
||||||
error: () => Promise<typeof import("./plugins/error.js")>;
|
error: () => Promise<typeof import("./plugins/error.js")>;
|
||||||
"py-editor": () => Promise<typeof import("./plugins/py-editor.js")>;
|
"py-editor": () => Promise<typeof import("./plugins/py-editor.js")>;
|
||||||
"py-game": () => Promise<typeof import("./plugins/py-game.js")>;
|
|
||||||
"py-terminal": () => Promise<typeof import("./plugins/py-terminal.js")>;
|
"py-terminal": () => Promise<typeof import("./plugins/py-terminal.js")>;
|
||||||
};
|
};
|
||||||
export default _default;
|
export default _default;
|
||||||
|
|||||||
9
core/types/plugins/codemirror.d.ts
vendored
9
core/types/plugins/codemirror.d.ts
vendored
@@ -1,9 +0,0 @@
|
|||||||
declare namespace _default {
|
|
||||||
const core: Promise<typeof import("../3rd-party/codemirror.js")>;
|
|
||||||
const state: Promise<typeof import("../3rd-party/codemirror_state.js")>;
|
|
||||||
const python: Promise<typeof import("../3rd-party/codemirror_lang-python.js")>;
|
|
||||||
const language: Promise<typeof import("../3rd-party/codemirror_language.js")>;
|
|
||||||
const view: Promise<typeof import("../3rd-party/codemirror_view.js")>;
|
|
||||||
const commands: Promise<typeof import("../3rd-party/codemirror_commands.js")>;
|
|
||||||
}
|
|
||||||
export default _default;
|
|
||||||
1
core/types/plugins/py-game.d.ts
vendored
1
core/types/plugins/py-game.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
export {};
|
|
||||||
3
core/types/stdlib/pyscript.d.ts
vendored
3
core/types/stdlib/pyscript.d.ts
vendored
@@ -2,11 +2,10 @@ declare namespace _default {
|
|||||||
let pyscript: {
|
let pyscript: {
|
||||||
"__init__.py": string;
|
"__init__.py": string;
|
||||||
"display.py": string;
|
"display.py": string;
|
||||||
"events.py": string;
|
"event_handling.py": string;
|
||||||
"fetch.py": string;
|
"fetch.py": string;
|
||||||
"ffi.py": string;
|
"ffi.py": string;
|
||||||
"flatted.py": string;
|
"flatted.py": string;
|
||||||
"fs.py": string;
|
|
||||||
"magic_js.py": string;
|
"magic_js.py": string;
|
||||||
"media.py": string;
|
"media.py": string;
|
||||||
"storage.py": string;
|
"storage.py": string;
|
||||||
|
|||||||
11
core/types/sync.d.ts
vendored
11
core/types/sync.d.ts
vendored
@@ -5,16 +5,5 @@ declare namespace _default {
|
|||||||
* @param {number} seconds The number of seconds to sleep.
|
* @param {number} seconds The number of seconds to sleep.
|
||||||
*/
|
*/
|
||||||
function sleep(seconds: number): Promise<any>;
|
function sleep(seconds: number): Promise<any>;
|
||||||
/**
|
|
||||||
* Ask a user action via dialog and returns the directory handler once granted.
|
|
||||||
* @param {string} uid
|
|
||||||
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function storeFSHandler(uid: string, options?: {
|
|
||||||
id?: string;
|
|
||||||
mode?: "read" | "readwrite";
|
|
||||||
hint?: "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
|
|
||||||
}): boolean;
|
|
||||||
}
|
}
|
||||||
export default _default;
|
export default _default;
|
||||||
|
|||||||
@@ -39,15 +39,6 @@
|
|||||||
#header p {
|
#header p {
|
||||||
font-size: 1.4em;
|
font-size: 1.4em;
|
||||||
}
|
}
|
||||||
#header > svg {
|
|
||||||
max-width: 480px;
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 400px) {
|
|
||||||
#header > svg {
|
|
||||||
width: 256px;
|
|
||||||
height: 87px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
function copyToClipboard() {
|
function copyToClipboard() {
|
||||||
@@ -62,7 +53,12 @@
|
|||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 173">
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="512"
|
||||||
|
height="173"
|
||||||
|
viewBox="0 0 512 173"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
fill="#fda703"
|
fill="#fda703"
|
||||||
d="M170.14 16.147c12.97-12.966 34.252-21.918 51.437-11.75c3.776 2.234 7.3 5.063 10.242 8.321c9.075 10.047 11.853 24.04 11.915 37.193c.125 26.222-9.355 56.093-34.555 68.782c-9.635 4.851-20.398 6.592-31.126 6.122c-6.042-.265-12.118-1.49-18.162-1.568l-.567-.004v46.425h-20.047V3.222h20.047v26.642h.264c2.19-5.078 6.687-9.854 10.551-13.717m1.43 20.312c-1.706 1.975-3.301 4.022-4.862 6.111l-1.033 1.386c-2.247 3-4.878 6.491-6.095 9.912c-.91 2.559-.256 6.27-.256 8.969v42.469l.845.173c13.804 2.808 27.723 4.557 40.305-3.454c17.28-11.002 22.71-32.58 21.39-51.85l-.04-.559c-.678-9.13-2.678-18.348-9.484-25.017c-13.438-13.167-31.313.912-40.77 11.86M261.67 3.222c.852 2.961 2.596 5.653 3.875 8.441c3.134 6.827 6.502 13.549 9.773 20.311c9.214 19.044 18.41 38.111 27.444 57.24c2.807 5.944 5.718 11.838 8.551 17.768l1.21 2.544c1.239 2.626 3.65 5.807 4.053 8.705c.196 1.397-.933 3.115-1.467 4.396l-.036.088a79.5 79.5 0 0 1-5.605 11.343l-.86 1.439c-1.961 3.273-4.013 6.592-6.603 9.376l-.392.42c-2.033 2.167-4.196 4.296-6.706 5.896c-11.047 7.047-23.326 5.408-35.347 2.652v17.674l.842.107c13.763 1.742 27.7 2.564 40.572-3.871c13.107-6.554 21-19.512 27.502-32.11c11.882-23.024 20.787-47.708 30.778-71.61l.39-.93C365.137 50.016 370.51 36.88 376 23.797c1.806-4.304 3.56-8.631 5.389-12.925c1.036-2.433 2.49-5.036 2.94-7.65h-14.243c-1.433 0-4.197-.528-5.437.271c-1.488.96-2.137 4.48-2.815 6.06c-2.363 5.496-4.505 11.088-6.787 16.618c-6.304 15.28-12.598 30.568-18.778 45.898c-2.72 6.742-6.71 13.77-8.389 20.839h-.527l-3.89-8.177l-6.857-13.98l-21.368-43.789l-8.31-16.882l-3.532-6.587l-5.898-.27zM0 64.683c2.398 1.987 5.918 3.156 8.705 4.55l71.748 35.874c7.172 3.586 14.3 7.25 21.46 10.856l3.072 1.542c3.547 1.773 7.697 4.777 11.606 5.474v-12.925l-.27-5.173l-6.06-3.334l-15.3-7.65l-40.358-20.31l-17.674-8.904l17.674-8.77l40.095-19.917l15.563-7.78l6.06-3.268l.27-5.108V6.651c-4.078.727-8.432 3.888-12.134 5.738L26.378 51.43C17.63 55.804 8.446 59.754 0 64.683M395.672 6.651v12.926c0 1.382-.496 3.972.272 5.172c.958 1.5 4.234 2.423 5.795 3.203l15.036 7.517c13.292 6.646 26.536 13.4 39.83 20.047c5.92 2.96 12.1 7.333 18.466 9.167v.528c-6.835 1.969-13.68 6.511-20.048 9.695l-38.512 19.256c-4.635 2.317-9.224 4.866-13.98 6.923l-.184.078c-1.805.75-5.401 1.885-6.403 3.454c-.768 1.2-.272 3.79-.272 5.173v13.189l14.508-7.057l77.025-38.512L512 64.947c-2.654-2.68-7.705-4.182-11.079-5.868L404.905 11.07c-2.965-1.483-6.044-3.5-9.233-4.419"
|
d="M170.14 16.147c12.97-12.966 34.252-21.918 51.437-11.75c3.776 2.234 7.3 5.063 10.242 8.321c9.075 10.047 11.853 24.04 11.915 37.193c.125 26.222-9.355 56.093-34.555 68.782c-9.635 4.851-20.398 6.592-31.126 6.122c-6.042-.265-12.118-1.49-18.162-1.568l-.567-.004v46.425h-20.047V3.222h20.047v26.642h.264c2.19-5.078 6.687-9.854 10.551-13.717m1.43 20.312c-1.706 1.975-3.301 4.022-4.862 6.111l-1.033 1.386c-2.247 3-4.878 6.491-6.095 9.912c-.91 2.559-.256 6.27-.256 8.969v42.469l.845.173c13.804 2.808 27.723 4.557 40.305-3.454c17.28-11.002 22.71-32.58 21.39-51.85l-.04-.559c-.678-9.13-2.678-18.348-9.484-25.017c-13.438-13.167-31.313.912-40.77 11.86M261.67 3.222c.852 2.961 2.596 5.653 3.875 8.441c3.134 6.827 6.502 13.549 9.773 20.311c9.214 19.044 18.41 38.111 27.444 57.24c2.807 5.944 5.718 11.838 8.551 17.768l1.21 2.544c1.239 2.626 3.65 5.807 4.053 8.705c.196 1.397-.933 3.115-1.467 4.396l-.036.088a79.5 79.5 0 0 1-5.605 11.343l-.86 1.439c-1.961 3.273-4.013 6.592-6.603 9.376l-.392.42c-2.033 2.167-4.196 4.296-6.706 5.896c-11.047 7.047-23.326 5.408-35.347 2.652v17.674l.842.107c13.763 1.742 27.7 2.564 40.572-3.871c13.107-6.554 21-19.512 27.502-32.11c11.882-23.024 20.787-47.708 30.778-71.61l.39-.93C365.137 50.016 370.51 36.88 376 23.797c1.806-4.304 3.56-8.631 5.389-12.925c1.036-2.433 2.49-5.036 2.94-7.65h-14.243c-1.433 0-4.197-.528-5.437.271c-1.488.96-2.137 4.48-2.815 6.06c-2.363 5.496-4.505 11.088-6.787 16.618c-6.304 15.28-12.598 30.568-18.778 45.898c-2.72 6.742-6.71 13.77-8.389 20.839h-.527l-3.89-8.177l-6.857-13.98l-21.368-43.789l-8.31-16.882l-3.532-6.587l-5.898-.27zM0 64.683c2.398 1.987 5.918 3.156 8.705 4.55l71.748 35.874c7.172 3.586 14.3 7.25 21.46 10.856l3.072 1.542c3.547 1.773 7.697 4.777 11.606 5.474v-12.925l-.27-5.173l-6.06-3.334l-15.3-7.65l-40.358-20.31l-17.674-8.904l17.674-8.77l40.095-19.917l15.563-7.78l6.06-3.268l.27-5.108V6.651c-4.078.727-8.432 3.888-12.134 5.738L26.378 51.43C17.63 55.804 8.446 59.754 0 64.683M395.672 6.651v12.926c0 1.382-.496 3.972.272 5.172c.958 1.5 4.234 2.423 5.795 3.203l15.036 7.517c13.292 6.646 26.536 13.4 39.83 20.047c5.92 2.96 12.1 7.333 18.466 9.167v.528c-6.835 1.969-13.68 6.511-20.048 9.695l-38.512 19.256c-4.635 2.317-9.224 4.866-13.98 6.923l-.184.078c-1.805.75-5.401 1.885-6.403 3.454c-.768 1.2-.272 3.79-.272 5.173v13.189l14.508-7.057l77.025-38.512L512 64.947c-2.654-2.68-7.705-4.182-11.079-5.868L404.905 11.07c-2.965-1.483-6.044-3.5-9.233-4.419"
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
[tool.codespell]
|
[tool.codespell]
|
||||||
ignore-words-list = "afterall"
|
ignore-words-list = "afterall"
|
||||||
skip = "*.js,*.json"
|
skip = "*.js,*.json"
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
line-length = 114
|
|
||||||
lint.select = ["C4", "C90", "E", "EM", "F", "PIE", "PYI", "PLC", "Q", "RET", "W"]
|
|
||||||
lint.ignore = ["E402", "E722", "E731", "E741", "F401", "F704", "F811", "F821"]
|
|
||||||
lint.mccabe.max-complexity = 27
|
|
||||||
|
|||||||
Reference in New Issue
Block a user