mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 10:47:35 -05:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57b1440a10 | ||
|
|
fc53356a1d | ||
|
|
5be99456f0 | ||
|
|
7adedcc704 | ||
|
|
d143b229ed | ||
|
|
0d74a60227 | ||
|
|
ce923a354f | ||
|
|
7e65836423 | ||
|
|
796373cfa6 | ||
|
|
0a1d3bb678 | ||
|
|
4e43d3e92d | ||
|
|
5acc2afaf3 | ||
|
|
56c64cbee7 | ||
|
|
4ff02a24d1 | ||
|
|
a5dc94792b | ||
|
|
0db79e0f02 | ||
|
|
283eabdb30 |
@@ -25,13 +25,13 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 24.10.0
|
rev: 25.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
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.3.0
|
rev: v2.4.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell # See 'pyproject.toml' for args
|
- id: codespell # See 'pyproject.toml' for args
|
||||||
exclude: \.js\.map$
|
exclude: \.js\.map$
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -70,6 +70,10 @@ 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"
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Read the [contributing guide](https://docs.pyscript.net/latest/contributing/)
|
|||||||
to learn about our development process, reporting bugs and improvements,
|
to learn about our development process, reporting bugs and improvements,
|
||||||
creating issues and asking questions.
|
creating issues and asking questions.
|
||||||
|
|
||||||
Check out the [developing process](https://docs.pyscript.net/latest/developers/)
|
Check out the [development process](https://docs.pyscript.net/latest/developers/)
|
||||||
documentation for more information on how to setup your development environment.
|
documentation for more information on how to setup your development environment.
|
||||||
|
|
||||||
## Governance
|
## Governance
|
||||||
|
|||||||
847
core/package-lock.json
generated
847
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.13",
|
"version": "0.6.25",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "PyScript",
|
"description": "PyScript",
|
||||||
"module": "./index.js",
|
"module": "./index.js",
|
||||||
@@ -62,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.4",
|
"polyscript": "^0.16.10",
|
||||||
"sabayon": "^0.6.0",
|
"sabayon": "^0.6.6",
|
||||||
"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.7.1",
|
"@codemirror/commands": "^6.8.0",
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.7",
|
||||||
"@codemirror/language": "^6.10.3",
|
"@codemirror/language": "^6.10.8",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.34.1",
|
"@codemirror/view": "^6.36.2",
|
||||||
"@playwright/test": "1.45.3",
|
"@playwright/test": "1.45.3",
|
||||||
"@rollup/plugin-commonjs": "^28.0.1",
|
"@rollup/plugin-commonjs": "^28.0.2",
|
||||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
"@rollup/plugin-node-resolve": "^16.0.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",
|
||||||
"bun": "^1.1.33",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"chokidar": "^4.0.1",
|
"bun": "^1.2.2",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
"codedent": "^0.1.2",
|
"codedent": "^0.1.2",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"eslint": "^9.13.0",
|
"eslint": "^9.19.0",
|
||||||
"flatted": "^3.3.1",
|
"flatted": "^3.3.2",
|
||||||
"rollup": "^4.24.2",
|
"rollup": "^4.34.3",
|
||||||
"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.6.3",
|
"typescript": "^5.7.3",
|
||||||
"xterm": "^5.3.0",
|
"xterm-readline": "^1.1.2"
|
||||||
"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.js": resolve("@xterm/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(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
"xterm.css": fetch(
|
||||||
(b) => b.text(),
|
`${CDN}/@xterm/xterm@${v("@xterm/xterm")}/css/xterm.min.css`,
|
||||||
),
|
).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@5.3.0/css/xterm.css
|
* Original file: /npm/@xterm/xterm@5.5.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,.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}
|
.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}
|
||||||
|
|||||||
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}}
|
||||||
*/
|
*/
|
||||||
const configDetails = async (config, type) => {
|
export 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 = "",
|
||||||
|
|||||||
@@ -73,3 +73,8 @@ mpy-config {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
animation: spinner 0.6s linear infinite;
|
animation: spinner 0.6s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
py-terminal span,
|
||||||
|
mpy-terminal span {
|
||||||
|
letter-spacing: 0 !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ import {
|
|||||||
inputFailure,
|
inputFailure,
|
||||||
} from "./hooks.js";
|
} from "./hooks.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 };
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
// ⚠️ 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 */
|
||||||
@@ -20,6 +25,11 @@ 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 */
|
||||||
|
|||||||
31
core/src/plugins/codemirror.js
Normal file
31
core/src/plugins/codemirror.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// 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"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -5,8 +5,15 @@ const { stringify } = JSON;
|
|||||||
|
|
||||||
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
||||||
|
|
||||||
const donkey = ({ type = "py", persistent, terminal, config }) => {
|
const donkey = ({
|
||||||
const args = persistent ? ["globals()", "__locals__"] : ["{}", "{}"];
|
type = "py",
|
||||||
|
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([
|
||||||
@@ -45,6 +52,7 @@ const donkey = ({ type = "py", persistent, terminal, config }) => {
|
|||||||
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,6 +2,7 @@
|
|||||||
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:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
||||||
|
|
||||||
@@ -194,14 +195,12 @@ const init = async (script, type, interpreter) => {
|
|||||||
{ keymap },
|
{ keymap },
|
||||||
{ defaultKeymap, indentWithTab },
|
{ defaultKeymap, indentWithTab },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
codemirror.core,
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
codemirror.state,
|
||||||
import(
|
codemirror.python,
|
||||||
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
codemirror.language,
|
||||||
),
|
codemirror.view,
|
||||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"),
|
codemirror.commands,
|
||||||
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");
|
||||||
|
|||||||
68
core/src/plugins/py-game.js
Normal file
68
core/src/plugins/py-game.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { dedent, define } from "polyscript/exports";
|
||||||
|
|
||||||
|
import { stdlib } from "../core.js";
|
||||||
|
import { configDetails } from "../config.js";
|
||||||
|
import { getText } from "../fetch.js";
|
||||||
|
|
||||||
|
let toBeWarned = true;
|
||||||
|
|
||||||
|
const hooks = {
|
||||||
|
main: {
|
||||||
|
onReady: async (wrap, script) => {
|
||||||
|
if (toBeWarned) {
|
||||||
|
toBeWarned = false;
|
||||||
|
console.warn("⚠️ EXPERIMENTAL `py-game` FEATURE");
|
||||||
|
}
|
||||||
|
if (script.hasAttribute("config")) {
|
||||||
|
const value = script.getAttribute("config");
|
||||||
|
const { json, toml, text } = configDetails(value);
|
||||||
|
let config = {};
|
||||||
|
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) {
|
||||||
|
const micropip = wrap.interpreter.pyimport("micropip");
|
||||||
|
await micropip.install(config.packages, {
|
||||||
|
keep_going: true,
|
||||||
|
});
|
||||||
|
micropip.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
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,
|
||||||
|
});
|
||||||
@@ -34,6 +34,8 @@ 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", {
|
||||||
@@ -71,6 +73,7 @@ export default async (element) => {
|
|||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: "block",
|
cursorStyle: "block",
|
||||||
|
lineHeight: 1.2,
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream;
|
let stream;
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ 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;
|
||||||
@@ -136,6 +137,18 @@ 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,8 +30,6 @@
|
|||||||
# 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.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,
|
||||||
@@ -43,19 +41,11 @@ 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
|
||||||
|
|
||||||
try:
|
|
||||||
from pyscript.event_handling import when
|
|
||||||
except:
|
|
||||||
# TODO: should we remove this? Or at the very least, we should capture
|
|
||||||
# the traceback otherwise it's very hard to debug
|
|
||||||
from pyscript.util import NotSupported
|
|
||||||
|
|
||||||
when = NotSupported(
|
|
||||||
"pyscript.when", "pyscript.when currently not available with this interpreter"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
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(event_type=None, selector=None):
|
|
||||||
"""
|
|
||||||
Decorates a function and passes py-* events to the decorated function
|
|
||||||
The events might or not be an argument of the decorated function
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
|
|
||||||
from pyscript.web import Element, ElementCollection
|
|
||||||
|
|
||||||
if isinstance(selector, str):
|
|
||||||
elements = document.querySelectorAll(selector)
|
|
||||||
# TODO: This is a hack that will be removed when pyscript becomes a package
|
|
||||||
# and we can better manage the imports without circular dependencies
|
|
||||||
elif isinstance(selector, Element):
|
|
||||||
elements = [selector._dom_element]
|
|
||||||
elif isinstance(selector, ElementCollection):
|
|
||||||
elements = [el._dom_element for el in selector]
|
|
||||||
else:
|
|
||||||
if isinstance(selector, list):
|
|
||||||
elements = selector
|
|
||||||
else:
|
|
||||||
elements = [selector]
|
|
||||||
|
|
||||||
try:
|
|
||||||
sig = inspect.signature(func)
|
|
||||||
# Function doesn't receive events
|
|
||||||
if not sig.parameters:
|
|
||||||
|
|
||||||
# Function is async: must be awaited
|
|
||||||
if inspect.iscoroutinefunction(func):
|
|
||||||
|
|
||||||
async def wrapper(*args, **kwargs):
|
|
||||||
await func()
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
func()
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
for el in elements:
|
|
||||||
add_event_listener(el, event_type, wrapper)
|
|
||||||
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
166
core/src/stdlib/pyscript/events.py
Normal file
166
core/src/stdlib/pyscript/events.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
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:
|
||||||
|
raise ValueError("Listener must be callable or awaitable.")
|
||||||
|
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
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
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
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)
|
||||||
@@ -31,3 +37,22 @@ 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,7 +2,8 @@
|
|||||||
|
|
||||||
# `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 pyscript import document, when, Event # NOQA
|
||||||
|
from pyscript.ffi import create_proxy
|
||||||
|
|
||||||
|
|
||||||
def wrap_dom_element(dom_element):
|
def wrap_dom_element(dom_element):
|
||||||
@@ -68,6 +69,18 @@ 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)
|
||||||
|
|
||||||
@@ -75,7 +88,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, **kwargs)
|
self.update(classes=classes, style=style, **properties)
|
||||||
|
|
||||||
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."""
|
||||||
@@ -93,13 +106,21 @@ class Element:
|
|||||||
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]
|
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):
|
||||||
@@ -119,8 +140,33 @@ class Element:
|
|||||||
if name.endswith("_"):
|
if name.endswith("_"):
|
||||||
name = name[:-1]
|
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_"):
|
||||||
|
raise ValueError("Event names must start with 'on_'.")
|
||||||
|
event_name = name[3:] # Remove the "on_" prefix.
|
||||||
|
if not hasattr(self._dom_element, event_name):
|
||||||
|
raise ValueError(f"Element has no '{event_name}' event.")
|
||||||
|
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`."""
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
17
core/tests/javascript/pyodide-lockfile/index.html
Normal file
17
core/tests/javascript/pyodide-lockfile/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../../../dist/core.css">
|
||||||
|
<script 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>
|
||||||
@@ -139,6 +139,25 @@ 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');
|
||||||
|
|||||||
13
core/tests/manual/emoji.html
Normal file
13
core/tests/manual/emoji.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!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>
|
||||||
17
core/tests/manual/emoji.py
Normal file
17
core/tests/manual/emoji.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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, "]")
|
||||||
30
core/tests/manual/game/aliens.css
Normal file
30
core/tests/manual/game/aliens.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/* (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;
|
||||||
|
}
|
||||||
399
core/tests/manual/game/aliens.py
Normal file
399
core/tests/manual/game/aliens.py
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
"""(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():
|
||||||
|
raise SystemExit("Sorry, extended image module required")
|
||||||
|
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
raise SystemExit(f'Could not load image "{file}" {pygame.get_error()}')
|
||||||
|
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:
|
||||||
|
sound = pygame.mixer.Sound(file)
|
||||||
|
return sound
|
||||||
|
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 SCORE != self.lastscore:
|
||||||
|
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
|
||||||
|
elif event.type == pygame.KEYDOWN:
|
||||||
|
if 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).keys():
|
||||||
|
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()
|
||||||
19
core/tests/manual/game/index.html
Normal file
19
core/tests/manual/game/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!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"></script>
|
||||||
|
<div class="demo">
|
||||||
|
<div class="demo-header">pygame.examples.aliens</div>
|
||||||
|
<div class="demo-content">
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
core/tests/manual/issue-2246/index.html
Normal file
11
core/tests/manual/issue-2246/index.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!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
core/tests/manual/issue-2246/main.py
Normal file
1
core/tests/manual/issue-2246/main.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
print(input("What food would you like me to get from the shop? "))
|
||||||
@@ -62,6 +62,7 @@
|
|||||||
<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.8/upytest.py": "",
|
"https://raw.githubusercontent.com/ntoll/upytest/1.0.9/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",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"./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_when.py": "tests/test_when.py",
|
"./tests/test_events.py": "tests/test_events.py",
|
||||||
"./tests/test_window.py": "tests/test_window.py"
|
"./tests/test_window.py": "tests/test_window.py"
|
||||||
},
|
},
|
||||||
"js_modules": {
|
"js_modules": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/upytest.py": "",
|
"https://raw.githubusercontent.com/ntoll/upytest/1.0.9/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",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"./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_when.py": "tests/test_when.py",
|
"./tests/test_events.py": "tests/test_events.py",
|
||||||
"./tests/test_window.py": "tests/test_window.py"
|
"./tests/test_window.py": "tests/test_window.py"
|
||||||
},
|
},
|
||||||
"js_modules": {
|
"js_modules": {
|
||||||
|
|||||||
360
core/tests/python/tests/test_events.py
Normal file
360
core/tests/python/tests/test_events.py
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
"""
|
||||||
|
Tests for the when function and Event class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import upytest
|
||||||
|
from pyscript import RUNNING_IN_WORKER, web, Event, when
|
||||||
|
|
||||||
|
|
||||||
|
def get_container():
|
||||||
|
return web.page.find("#test-element-container")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
container = get_container()
|
||||||
|
container.innerHTML = ""
|
||||||
|
|
||||||
|
|
||||||
|
def teardown():
|
||||||
|
container = get_container()
|
||||||
|
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():
|
||||||
|
"""
|
||||||
|
When the decorated function takes a single parameter,
|
||||||
|
it should be passed the event object.
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
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():
|
||||||
|
"""
|
||||||
|
When the decorated function takes no parameters (not including 'self'),
|
||||||
|
it should be called without the event object.
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
def foo():
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called is True
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
||||||
|
"""
|
||||||
|
When decorating a function twice, both should function
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called1 = False
|
||||||
|
called2 = False
|
||||||
|
call_flag1 = asyncio.Event()
|
||||||
|
call_flag2 = asyncio.Event()
|
||||||
|
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo1(evt):
|
||||||
|
nonlocal called1
|
||||||
|
called1 = True
|
||||||
|
call_flag1.set()
|
||||||
|
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo2(evt):
|
||||||
|
nonlocal called2
|
||||||
|
called2 = True
|
||||||
|
call_flag2.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag1.wait()
|
||||||
|
await call_flag2.wait()
|
||||||
|
assert called1
|
||||||
|
assert called2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_multiple_elements():
|
||||||
|
"""
|
||||||
|
The @when decorator's selector should successfully select multiple
|
||||||
|
DOM elements
|
||||||
|
"""
|
||||||
|
btn1 = web.button(
|
||||||
|
"foo_button1",
|
||||||
|
id="foo_id1",
|
||||||
|
classes=[
|
||||||
|
"foo_class",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
btn2 = web.button(
|
||||||
|
"foo_button2",
|
||||||
|
id="foo_id2",
|
||||||
|
classes=[
|
||||||
|
"foo_class",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn1)
|
||||||
|
container.append(btn2)
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
call_flag1 = asyncio.Event()
|
||||||
|
call_flag2 = asyncio.Event()
|
||||||
|
|
||||||
|
@when("click", selector=".foo_class")
|
||||||
|
def foo(evt):
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
if evt.target.id == "foo_id1":
|
||||||
|
call_flag1.set()
|
||||||
|
else:
|
||||||
|
call_flag2.set()
|
||||||
|
|
||||||
|
assert counter == 0, counter
|
||||||
|
btn1.click()
|
||||||
|
await call_flag1.wait()
|
||||||
|
assert counter == 1, counter
|
||||||
|
btn2.click()
|
||||||
|
await call_flag2.wait()
|
||||||
|
assert counter == 2, counter
|
||||||
|
|
||||||
|
|
||||||
|
@upytest.skip(
|
||||||
|
"Only works in Pyodide on main thread",
|
||||||
|
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
|
||||||
|
)
|
||||||
|
def test_when_decorator_invalid_selector():
|
||||||
|
"""
|
||||||
|
When the selector parameter of @when is invalid, it should raise an error.
|
||||||
|
"""
|
||||||
|
if upytest.is_micropython:
|
||||||
|
from jsffi import JsException
|
||||||
|
else:
|
||||||
|
from pyodide.ffi import JsException
|
||||||
|
|
||||||
|
with upytest.raises(JsException) as e:
|
||||||
|
|
||||||
|
@when("click", selector="#.bad")
|
||||||
|
def foo(evt): ...
|
||||||
|
|
||||||
|
assert "'#.bad' is not a valid selector" in str(e.exception), str(e.exception)
|
||||||
|
|
||||||
|
|
||||||
|
def test_when_decorates_an_event():
|
||||||
|
"""
|
||||||
|
When the @when decorator is used on a function to handle an Event instance,
|
||||||
|
the function should be called when the Event object is triggered.
|
||||||
|
"""
|
||||||
|
|
||||||
|
whenable = Event()
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
# When as a decorator.
|
||||||
|
@when(whenable)
|
||||||
|
def handler(result):
|
||||||
|
"""
|
||||||
|
A function that should be called when the whenable object is triggered.
|
||||||
|
|
||||||
|
The result generated by the whenable object should be passed to the
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
assert result == "ok"
|
||||||
|
|
||||||
|
# The function should not be called until the whenable object is triggered.
|
||||||
|
assert counter == 0
|
||||||
|
# Trigger the whenable object.
|
||||||
|
whenable.trigger("ok")
|
||||||
|
# The function should have been called when the whenable object was
|
||||||
|
# triggered.
|
||||||
|
assert counter == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_when_called_with_an_event_and_handler():
|
||||||
|
"""
|
||||||
|
The when function should be able to be called with an Event object,
|
||||||
|
and a handler function.
|
||||||
|
"""
|
||||||
|
whenable = Event()
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
def handler(result):
|
||||||
|
"""
|
||||||
|
A function that should be called when the whenable object is triggered.
|
||||||
|
|
||||||
|
The result generated by the whenable object should be passed to the
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
assert result == "ok"
|
||||||
|
|
||||||
|
# When as a function.
|
||||||
|
when(whenable, handler)
|
||||||
|
|
||||||
|
# The function should not be called until the whenable object is triggered.
|
||||||
|
assert counter == 0
|
||||||
|
# Trigger the whenable object.
|
||||||
|
whenable.trigger("ok")
|
||||||
|
# The function should have been called when the whenable object was
|
||||||
|
# triggered.
|
||||||
|
assert counter == 1
|
||||||
48
core/tests/python/tests/test_util.py
Normal file
48
core/tests/python/tests/test_util.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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,6 +164,57 @@ 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]
|
||||||
@@ -227,11 +278,15 @@ 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.find("button")
|
buttons_collection = web.page["button"]
|
||||||
|
|
||||||
@when("click", buttons_collection)
|
@when("click", buttons_collection)
|
||||||
def on_click(event):
|
def on_click(event):
|
||||||
@@ -249,6 +304,28 @@ 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:
|
||||||
|
|
||||||
@@ -759,14 +836,13 @@ 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 on Pyodide in worker.",
|
"Flakey in worker.",
|
||||||
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
|
skip_when=RUNNING_IN_WORKER,
|
||||||
)
|
)
|
||||||
async def test_img(self):
|
async def test_img(self):
|
||||||
"""
|
"""
|
||||||
This test contains a bespoke version of the _create_el_and_basic_asserts
|
This test, thanks to downloading an image from the internet, is flakey
|
||||||
function so we can await asyncio.sleep if in a worker, so the DOM state
|
when run in a worker. It's skipped when running in a worker.
|
||||||
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",
|
||||||
@@ -774,39 +850,7 @@ 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
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
"""
|
|
||||||
Tests for the pyscript.when decorator.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import upytest
|
|
||||||
from pyscript import RUNNING_IN_WORKER, web
|
|
||||||
|
|
||||||
|
|
||||||
def get_container():
|
|
||||||
return web.page.find("#test-element-container")[0]
|
|
||||||
|
|
||||||
|
|
||||||
def setup():
|
|
||||||
container = get_container()
|
|
||||||
container.innerHTML = ""
|
|
||||||
|
|
||||||
|
|
||||||
def teardown():
|
|
||||||
container = get_container()
|
|
||||||
container.innerHTML = ""
|
|
||||||
|
|
||||||
|
|
||||||
async def test_when_decorator_with_event():
|
|
||||||
"""
|
|
||||||
When the decorated function takes a single parameter,
|
|
||||||
it should be passed the event object
|
|
||||||
"""
|
|
||||||
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")
|
|
||||||
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():
|
|
||||||
"""
|
|
||||||
When the decorated function takes no parameters (not including 'self'),
|
|
||||||
it should be called without the event object
|
|
||||||
"""
|
|
||||||
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")
|
|
||||||
def foo():
|
|
||||||
nonlocal called
|
|
||||||
called = True
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
btn.click()
|
|
||||||
await call_flag.wait()
|
|
||||||
assert called
|
|
||||||
|
|
||||||
|
|
||||||
async def test_two_when_decorators():
|
|
||||||
"""
|
|
||||||
When decorating a function twice, both should function
|
|
||||||
"""
|
|
||||||
btn = web.button("foo_button", id="foo_id")
|
|
||||||
container = get_container()
|
|
||||||
container.append(btn)
|
|
||||||
|
|
||||||
called1 = False
|
|
||||||
called2 = False
|
|
||||||
call_flag1 = asyncio.Event()
|
|
||||||
call_flag2 = asyncio.Event()
|
|
||||||
|
|
||||||
@web.when("click", selector="#foo_id")
|
|
||||||
def foo1(evt):
|
|
||||||
nonlocal called1
|
|
||||||
called1 = True
|
|
||||||
call_flag1.set()
|
|
||||||
|
|
||||||
@web.when("click", selector="#foo_id")
|
|
||||||
def foo2(evt):
|
|
||||||
nonlocal called2
|
|
||||||
called2 = True
|
|
||||||
call_flag2.set()
|
|
||||||
|
|
||||||
btn.click()
|
|
||||||
await call_flag1.wait()
|
|
||||||
await call_flag2.wait()
|
|
||||||
assert called1
|
|
||||||
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():
|
|
||||||
"""
|
|
||||||
The @when decorator's selector should successfully select multiple
|
|
||||||
DOM elements
|
|
||||||
"""
|
|
||||||
btn1 = web.button(
|
|
||||||
"foo_button1",
|
|
||||||
id="foo_id1",
|
|
||||||
classes=[
|
|
||||||
"foo_class",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
btn2 = web.button(
|
|
||||||
"foo_button2",
|
|
||||||
id="foo_id2",
|
|
||||||
classes=[
|
|
||||||
"foo_class",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
container = get_container()
|
|
||||||
container.append(btn1)
|
|
||||||
container.append(btn2)
|
|
||||||
|
|
||||||
counter = 0
|
|
||||||
call_flag1 = asyncio.Event()
|
|
||||||
call_flag2 = asyncio.Event()
|
|
||||||
|
|
||||||
@web.when("click", selector=".foo_class")
|
|
||||||
def foo(evt):
|
|
||||||
nonlocal counter
|
|
||||||
counter += 1
|
|
||||||
if evt.target.id == "foo_id1":
|
|
||||||
call_flag1.set()
|
|
||||||
else:
|
|
||||||
call_flag2.set()
|
|
||||||
|
|
||||||
assert counter == 0, counter
|
|
||||||
btn1.click()
|
|
||||||
await call_flag1.wait()
|
|
||||||
assert counter == 1, counter
|
|
||||||
btn2.click()
|
|
||||||
await call_flag2.wait()
|
|
||||||
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(
|
|
||||||
"Only works in Pyodide on main thread",
|
|
||||||
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
|
|
||||||
)
|
|
||||||
def test_when_decorator_invalid_selector():
|
|
||||||
"""
|
|
||||||
When the selector parameter of @when is invalid, it should raise an error.
|
|
||||||
"""
|
|
||||||
if upytest.is_micropython:
|
|
||||||
from jsffi import JsException
|
|
||||||
else:
|
|
||||||
from pyodide.ffi import JsException
|
|
||||||
|
|
||||||
with upytest.raises(JsException) as e:
|
|
||||||
|
|
||||||
@web.when("click", selector="#.bad")
|
|
||||||
def foo(evt): ...
|
|
||||||
|
|
||||||
assert "'#.bad' is not a valid selector" in str(e.exception), str(e.exception)
|
|
||||||
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 b: any;
|
declare var v: any;
|
||||||
declare var I: boolean;
|
declare var k: boolean;
|
||||||
declare namespace r {
|
declare namespace i {
|
||||||
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: p;
|
layout: c;
|
||||||
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 p {
|
declare class c {
|
||||||
constructor(t: any);
|
constructor(t: any);
|
||||||
promptSize: any;
|
promptSize: any;
|
||||||
cursor: c;
|
cursor: u;
|
||||||
end: c;
|
end: u;
|
||||||
}
|
}
|
||||||
declare class c {
|
declare class u {
|
||||||
constructor(t: any, e: any);
|
constructor(t: any, e: any);
|
||||||
row: any;
|
row: any;
|
||||||
col: any;
|
col: any;
|
||||||
}
|
}
|
||||||
export { b as Readline, I as __esModule, r as default };
|
export { v as Readline, k as __esModule, i 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 i: any;
|
declare var D: any;
|
||||||
declare var s: any;
|
declare var R: any;
|
||||||
declare var t: {};
|
declare var L: {};
|
||||||
export { i as Terminal, s as __esModule, t as default };
|
export { D as Terminal, R as __esModule, L as default };
|
||||||
|
|||||||
5
core/types/config.d.ts
vendored
5
core/types/config.d.ts
vendored
@@ -1,2 +1,7 @@
|
|||||||
|
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,6 +7,7 @@ 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";
|
||||||
@@ -63,4 +64,4 @@ declare const exportedHooks: {
|
|||||||
};
|
};
|
||||||
declare const exportedConfig: {};
|
declare const exportedConfig: {};
|
||||||
declare const exportedWhenDefined: any;
|
declare const exportedWhenDefined: any;
|
||||||
export { stdlib, optional, inputFailure, TYPES, relative_url, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
export { codemirror, stdlib, optional, inputFailure, TYPES, relative_url, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
||||||
|
|||||||
2
core/types/plugins.d.ts
vendored
2
core/types/plugins.d.ts
vendored
@@ -1,8 +1,10 @@
|
|||||||
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
Normal file
9
core/types/plugins/codemirror.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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
Normal file
1
core/types/plugins/py-game.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
2
core/types/stdlib/pyscript.d.ts
vendored
2
core/types/stdlib/pyscript.d.ts
vendored
@@ -2,7 +2,7 @@ declare namespace _default {
|
|||||||
let pyscript: {
|
let pyscript: {
|
||||||
"__init__.py": string;
|
"__init__.py": string;
|
||||||
"display.py": string;
|
"display.py": string;
|
||||||
"event_handling.py": string;
|
"events.py": string;
|
||||||
"fetch.py": string;
|
"fetch.py": string;
|
||||||
"ffi.py": string;
|
"ffi.py": string;
|
||||||
"flatted.py": string;
|
"flatted.py": string;
|
||||||
|
|||||||
Reference in New Issue
Block a user