mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Compare commits
26 Commits
better-don
...
2025.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b13317d32f | ||
|
|
57b1440a10 | ||
|
|
fc53356a1d | ||
|
|
5be99456f0 | ||
|
|
7adedcc704 | ||
|
|
d143b229ed | ||
|
|
0d74a60227 | ||
|
|
ce923a354f | ||
|
|
7e65836423 | ||
|
|
796373cfa6 | ||
|
|
0a1d3bb678 | ||
|
|
4e43d3e92d | ||
|
|
5acc2afaf3 | ||
|
|
56c64cbee7 | ||
|
|
4ff02a24d1 | ||
|
|
a5dc94792b | ||
|
|
0db79e0f02 | ||
|
|
283eabdb30 | ||
|
|
3f19e00410 | ||
|
|
9233d5e45a | ||
|
|
fe580cd90b | ||
|
|
00e6cfed29 | ||
|
|
6b1330d28a | ||
|
|
5d751493f6 | ||
|
|
c3517f7973 | ||
|
|
b1c33b7f79 |
@@ -25,13 +25,13 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.10.0
|
||||
rev: 25.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ["-l", "88", "--skip-string-normalization"]
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell # See 'pyproject.toml' for args
|
||||
exclude: \.js\.map$
|
||||
|
||||
9
Makefile
9
Makefile
@@ -40,7 +40,7 @@ check-python:
|
||||
|
||||
# Check the environment, install the dependencies.
|
||||
setup: check-node check-npm check-python
|
||||
cd core && npm install && cd ..
|
||||
cd core && npm ci && cd ..
|
||||
ifeq ($(VIRTUAL_ENV),)
|
||||
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
|
||||
false
|
||||
@@ -55,12 +55,11 @@ clean:
|
||||
rm -rf .pytest_cache .coverage coverage.xml
|
||||
|
||||
# Build PyScript.
|
||||
build:
|
||||
build: precommit-check
|
||||
cd core && npx playwright install chromium && npm run build
|
||||
|
||||
# Update the dependencies.
|
||||
update:
|
||||
cd core && npm update && cd ..
|
||||
python -m pip install -r requirements.txt --upgrade
|
||||
|
||||
# Run the precommit checks (run eslint).
|
||||
@@ -71,6 +70,10 @@ precommit-check:
|
||||
test:
|
||||
cd core && npm run test:integration
|
||||
|
||||
# Serve the repository with the correct headers.
|
||||
serve:
|
||||
npx mini-coi .
|
||||
|
||||
# Format the code.
|
||||
fmt: fmt-py
|
||||
@echo "Format completed"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PyScript
|
||||
|
||||
## PyScrcipt is an open source platform for Python in the browser.
|
||||
## PyScript is an open source platform for Python in the browser.
|
||||
|
||||
Using PyScript is as simple as:
|
||||
|
||||
@@ -76,7 +76,7 @@ Read the [contributing guide](https://docs.pyscript.net/latest/contributing/)
|
||||
to learn about our development process, reporting bugs and improvements,
|
||||
creating issues and asking questions.
|
||||
|
||||
Check out the [developing process](https://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.
|
||||
|
||||
## Governance
|
||||
|
||||
907
core/package-lock.json
generated
907
core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pyscript/core",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.26",
|
||||
"type": "module",
|
||||
"description": "PyScript",
|
||||
"module": "./index.js",
|
||||
@@ -60,40 +60,41 @@
|
||||
"dependencies": {
|
||||
"@ungap/with-resolvers": "^0.1.0",
|
||||
"@webreflection/idb-map": "^0.3.2",
|
||||
"add-promise-listener": "^0.1.3",
|
||||
"basic-devtools": "^0.1.6",
|
||||
"polyscript": "^0.16.2",
|
||||
"sabayon": "^0.5.2",
|
||||
"polyscript": "^0.16.11",
|
||||
"sabayon": "^0.6.6",
|
||||
"sticky-module": "^0.1.1",
|
||||
"to-json-callback": "^0.1.1",
|
||||
"type-checked-collections": "^0.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.7.0",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/language": "^6.10.3",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.34.1",
|
||||
"@codemirror/commands": "^6.8.0",
|
||||
"@codemirror/lang-python": "^6.1.7",
|
||||
"@codemirror/language": "^6.10.8",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.2",
|
||||
"@playwright/test": "1.45.3",
|
||||
"@rollup/plugin-commonjs": "^28.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.2",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@webreflection/toml-j0.4": "^1.1.3",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"bun": "^1.1.30",
|
||||
"chokidar": "^4.0.1",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"bun": "^1.2.2",
|
||||
"chokidar": "^4.0.3",
|
||||
"codedent": "^0.1.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"eslint": "^9.12.0",
|
||||
"flatted": "^3.3.1",
|
||||
"rollup": "^4.24.0",
|
||||
"eslint": "^9.19.0",
|
||||
"flatted": "^3.3.2",
|
||||
"rollup": "^4.34.4",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"static-handler": "^0.5.3",
|
||||
"string-width": "^7.2.0",
|
||||
"typescript": "^5.6.3",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-readline": "^1.1.1"
|
||||
"typescript": "^5.7.3",
|
||||
"xterm-readline": "^1.1.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -46,7 +46,7 @@ const modules = {
|
||||
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
||||
|
||||
// xterm
|
||||
"xterm.js": resolve("xterm"),
|
||||
"xterm.js": resolve("@xterm/xterm"),
|
||||
"xterm-readline.js": resolve("xterm-readline"),
|
||||
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||
b.text(),
|
||||
@@ -54,9 +54,9 @@ const modules = {
|
||||
"xterm_addon-web-links.js": fetch(
|
||||
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||
).then((b) => b.text()),
|
||||
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
||||
(b) => b.text(),
|
||||
),
|
||||
"xterm.css": fetch(
|
||||
`${CDN}/@xterm/xterm@${v("@xterm/xterm")}/css/xterm.min.css`,
|
||||
).then((b) => b.text()),
|
||||
|
||||
// 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.
|
||||
* 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
|
||||
*/
|
||||
.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
|
||||
* @returns {{json: boolean, toml: boolean, text: string}}
|
||||
*/
|
||||
const configDetails = async (config, type) => {
|
||||
export const configDetails = async (config, type) => {
|
||||
let text = config?.trim();
|
||||
// we only support an object as root config
|
||||
let url = "",
|
||||
@@ -56,7 +56,7 @@ const syntaxError = (type, url, { message }) => {
|
||||
const configs = new Map();
|
||||
|
||||
for (const [TYPE] of TYPES) {
|
||||
/** @type {Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
|
||||
/** @type {() => Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
|
||||
let plugins;
|
||||
|
||||
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
|
||||
@@ -135,6 +135,7 @@ for (const [TYPE] of TYPES) {
|
||||
|
||||
// parse all plugins and optionally ignore only
|
||||
// those flagged as "undesired" via `!` prefix
|
||||
plugins = async () => {
|
||||
const toBeAwaited = [];
|
||||
for (const [key, value] of Object.entries(allPlugins)) {
|
||||
if (error) {
|
||||
@@ -146,11 +147,12 @@ for (const [TYPE] of TYPES) {
|
||||
}
|
||||
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
||||
toBeAwaited.push(value().then(({ default: p }) => p));
|
||||
} else if (key === "error") {
|
||||
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
|
||||
}
|
||||
}
|
||||
|
||||
// assign plugins as Promise.all only if needed
|
||||
plugins = Promise.all(toBeAwaited);
|
||||
return await Promise.all(toBeAwaited);
|
||||
};
|
||||
|
||||
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||
}
|
||||
|
||||
@@ -73,3 +73,8 @@ mpy-config {
|
||||
background-color: #fff;
|
||||
animation: spinner 0.6s linear infinite;
|
||||
}
|
||||
|
||||
py-terminal span,
|
||||
mpy-terminal span {
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ import {
|
||||
inputFailure,
|
||||
} from "./hooks.js";
|
||||
|
||||
import codemirror from "./plugins/codemirror.js";
|
||||
export { codemirror };
|
||||
|
||||
import { stdlib, optional } from "./stdlib.js";
|
||||
export { stdlib, optional, inputFailure };
|
||||
|
||||
@@ -179,7 +182,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
// ensure plugins are bootstrapped already before custom type definition
|
||||
// NOTE: we cannot top-level await in here as plugins import other utilities
|
||||
// from core.js itself so that custom definition should not be blocking.
|
||||
plugins.then(() => {
|
||||
plugins().then(() => {
|
||||
// possible early errors sent by polyscript
|
||||
const errors = new Map();
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||
export default {
|
||||
codemirror: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/codemirror.js"
|
||||
),
|
||||
["deprecations-manager"]: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
@@ -20,6 +25,11 @@ export default {
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/py-editor.js"
|
||||
),
|
||||
["py-game"]: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/py-game.js"
|
||||
),
|
||||
["py-terminal"]: () =>
|
||||
import(
|
||||
/* 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"
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
// PyScript Derepcations Plugin
|
||||
import { hooks } from "../core.js";
|
||||
import { notify } from "./error.js";
|
||||
import { hooks } from "../core.js";
|
||||
|
||||
// react lazily on PyScript bootstrap
|
||||
hooks.main.onReady.add(checkDeprecations);
|
||||
|
||||
@@ -1,64 +1,121 @@
|
||||
import addPromiseListener from "add-promise-listener";
|
||||
import { assign, dedent } from "polyscript/exports";
|
||||
|
||||
const { stringify } = JSON;
|
||||
|
||||
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
||||
|
||||
export default (options = {}) => {
|
||||
const type = options.type || "py";
|
||||
const args = options.persistent
|
||||
? ["globals()", "__locals__"]
|
||||
: ["{}", "{}"];
|
||||
const donkey = ({
|
||||
type = "py",
|
||||
persistent,
|
||||
terminal,
|
||||
config,
|
||||
serviceWorker,
|
||||
}) => {
|
||||
const globals = terminal ? '{"__terminal__":__terminal__}' : "{}";
|
||||
const args = persistent ? ["globals()", "__locals__"] : [globals, "{}"];
|
||||
|
||||
const src = URL.createObjectURL(
|
||||
new Blob([
|
||||
dedent(`
|
||||
from pyscript import sync, config
|
||||
__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"
|
||||
__locals__ = {}
|
||||
if config["type"] == "py":
|
||||
import sys
|
||||
def __error__(_):
|
||||
info = sys.exc_info()
|
||||
return __message__(info[0], info[1])
|
||||
else:
|
||||
__error__ = lambda e: __message__(e.__class__, e.value)
|
||||
def execute(code):
|
||||
try: return ${invoke("exec", args)};
|
||||
except Exception as e: print(__error__(e));
|
||||
def evaluate(code):
|
||||
try: return ${invoke("eval", args)};
|
||||
except Exception as e: print(__error__(e));
|
||||
sync.execute = execute
|
||||
sync.evaluate = evaluate
|
||||
`),
|
||||
[
|
||||
// this array is to better minify this code once in production
|
||||
"from pyscript import sync, config",
|
||||
'__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"',
|
||||
"__locals__ = {}",
|
||||
'if config["type"] == "py":',
|
||||
" import sys",
|
||||
" def __error__(_):",
|
||||
" info = sys.exc_info()",
|
||||
" return __message__(info[0], info[1])",
|
||||
"else:",
|
||||
" __error__ = lambda e: __message__(e.__class__, e.value)",
|
||||
"def execute(code):",
|
||||
` try: return ${invoke("exec", args)};`,
|
||||
" except Exception as e: print(__error__(e));",
|
||||
"def evaluate(code):",
|
||||
` try: return ${invoke("eval", args)};`,
|
||||
" except Exception as e: print(__error__(e));",
|
||||
"sync.execute = execute",
|
||||
"sync.evaluate = evaluate",
|
||||
].join("\n"),
|
||||
]),
|
||||
);
|
||||
|
||||
// create the script that exposes the code to execute or evaluate
|
||||
const script = assign(document.createElement("script"), { type, src });
|
||||
|
||||
script.toggleAttribute("worker", true);
|
||||
script.toggleAttribute("terminal", true);
|
||||
if (options.terminal) script.setAttribute("target", options.terminal);
|
||||
if (options.config)
|
||||
script.setAttribute("config", JSON.stringify(options.config));
|
||||
if (terminal) script.setAttribute("target", terminal);
|
||||
if (config) {
|
||||
script.setAttribute(
|
||||
"config",
|
||||
typeof config === "string" ? config : stringify(config),
|
||||
);
|
||||
}
|
||||
if (serviceWorker) script.setAttribute("service-worker", serviceWorker);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
script.addEventListener(`${type}:done`, (event) => {
|
||||
event.stopPropagation();
|
||||
return addPromiseListener(
|
||||
document.body.appendChild(script),
|
||||
`${type}:done`,
|
||||
{ stopPropagation: true },
|
||||
).then(() => {
|
||||
URL.revokeObjectURL(src);
|
||||
return script;
|
||||
});
|
||||
};
|
||||
|
||||
const utils = async (options) => {
|
||||
const script = await donkey(options);
|
||||
const { xworker, process, terminal } = script;
|
||||
const { execute, evaluate } = xworker.sync;
|
||||
script.remove();
|
||||
resolve({
|
||||
return {
|
||||
xworker,
|
||||
process,
|
||||
execute: (code) => execute(dedent(code)),
|
||||
evaluate: (code) => evaluate(dedent(code)),
|
||||
clear: () => terminal.clear(),
|
||||
reset: () => terminal.reset(),
|
||||
kill: () => {
|
||||
xworker.terminate();
|
||||
terminal.dispose();
|
||||
},
|
||||
});
|
||||
});
|
||||
document.body.append(script);
|
||||
});
|
||||
terminal,
|
||||
execute,
|
||||
evaluate,
|
||||
};
|
||||
};
|
||||
|
||||
export default async (options = {}) => {
|
||||
let farmer = await utils(options);
|
||||
let working = false;
|
||||
const kill = () => {
|
||||
if (farmer) {
|
||||
farmer.xworker.terminate();
|
||||
farmer.terminal.dispose();
|
||||
farmer = null;
|
||||
}
|
||||
working = false;
|
||||
};
|
||||
const reload = async () => {
|
||||
kill();
|
||||
farmer = await utils(options);
|
||||
};
|
||||
const asyncTask = (method) => async (code) => {
|
||||
// race condition ... a new task has been
|
||||
// assigned while the previous one didn't finish
|
||||
if (working) await reload();
|
||||
working = true;
|
||||
try {
|
||||
return await farmer[method](dedent(code));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
working = false;
|
||||
}
|
||||
};
|
||||
const asyncMethod = (method) => async () => {
|
||||
if (working) await reload();
|
||||
else farmer?.terminal[method]();
|
||||
};
|
||||
return {
|
||||
process: asyncTask("process"),
|
||||
execute: asyncTask("execute"),
|
||||
evaluate: asyncTask("evaluate"),
|
||||
clear: asyncMethod("clear"),
|
||||
reset: asyncMethod("reset"),
|
||||
kill,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// PyScript Error Plugin
|
||||
import { buffered } from "polyscript/exports";
|
||||
import { hooks } from "../core.js";
|
||||
|
||||
let dontBotherDOM = false;
|
||||
export function notOnDOM() {
|
||||
dontBotherDOM = true;
|
||||
}
|
||||
|
||||
hooks.main.onReady.add(function override(pyScript) {
|
||||
// be sure this override happens only once
|
||||
hooks.main.onReady.delete(override);
|
||||
@@ -8,13 +14,15 @@ hooks.main.onReady.add(function override(pyScript) {
|
||||
// trap generic `stderr` to propagate to it regardless
|
||||
const { stderr } = pyScript.io;
|
||||
|
||||
// override it with our own logic
|
||||
pyScript.io.stderr = (error, ...rest) => {
|
||||
const cb = (error, ...rest) => {
|
||||
notify(error.message || error);
|
||||
// let other plugins or stderr hook, if any, do the rest
|
||||
return stderr(error, ...rest);
|
||||
};
|
||||
|
||||
// override it with our own logic
|
||||
pyScript.io.stderr = pyScript.type === "py" ? cb : buffered(cb);
|
||||
|
||||
// be sure uncaught Python errors are also visible
|
||||
addEventListener("error", ({ message }) => {
|
||||
if (message.startsWith("Uncaught PythonError")) notify(message);
|
||||
@@ -30,6 +38,7 @@ hooks.main.onReady.add(function override(pyScript) {
|
||||
* @param {string} message
|
||||
*/
|
||||
export function notify(message) {
|
||||
if (dontBotherDOM) return;
|
||||
const div = document.createElement("div");
|
||||
div.className = "py-error";
|
||||
div.textContent = message;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.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>`;
|
||||
|
||||
@@ -194,14 +195,12 @@ const init = async (script, type, interpreter) => {
|
||||
{ keymap },
|
||||
{ defaultKeymap, indentWithTab },
|
||||
] = await Promise.all([
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
||||
),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
||||
codemirror.core,
|
||||
codemirror.state,
|
||||
codemirror.python,
|
||||
codemirror.language,
|
||||
codemirror.view,
|
||||
codemirror.commands,
|
||||
]);
|
||||
|
||||
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,
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
// PyScript pyodide terminal plugin
|
||||
import { hooks, inputFailure } from "../../core.js";
|
||||
import { defineProperties } from "polyscript/exports";
|
||||
import { hooks, inputFailure } from "../../core.js";
|
||||
|
||||
const bootstrapped = new WeakSet();
|
||||
|
||||
@@ -34,6 +34,8 @@ const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||
pyterminal_write(String(error.message || error));
|
||||
};
|
||||
|
||||
sync.pyterminal_stream_write = () => {};
|
||||
|
||||
// tiny shim of the code module with only interact
|
||||
// to bootstrap a REPL like environment
|
||||
interpreter.registerJsModule("code", {
|
||||
@@ -71,6 +73,7 @@ export default async (element) => {
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
lineHeight: 1.2,
|
||||
};
|
||||
|
||||
let stream;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// PyScript py-terminal plugin
|
||||
import { hooks } from "../../core.js";
|
||||
import { defineProperties } from "polyscript/exports";
|
||||
import { hooks } from "../../core.js";
|
||||
|
||||
const bootstrapped = new WeakSet();
|
||||
|
||||
@@ -126,6 +126,7 @@ export default async (element) => {
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
lineHeight: 1.2,
|
||||
});
|
||||
|
||||
xworker.sync.is_pyterminal = () => true;
|
||||
@@ -136,6 +137,18 @@ export default async (element) => {
|
||||
// setup remote thread JS/Python code for whenever the
|
||||
// worker is ready to become a terminal
|
||||
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 {
|
||||
// in the main case, just bootstrap XTerm without
|
||||
// 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.
|
||||
|
||||
from polyscript import lazy_py_modules as py_import
|
||||
from pyscript.display import HTML, display
|
||||
from pyscript.fetch import fetch
|
||||
from pyscript.magic_js import (
|
||||
RUNNING_IN_WORKER,
|
||||
PyWorker,
|
||||
@@ -43,19 +41,11 @@ from pyscript.magic_js import (
|
||||
sync,
|
||||
window,
|
||||
)
|
||||
from pyscript.display import HTML, display
|
||||
from pyscript.fetch import fetch
|
||||
from pyscript.storage import Storage, storage
|
||||
from pyscript.websocket import WebSocket
|
||||
from pyscript.events import when, Event
|
||||
|
||||
if not RUNNING_IN_WORKER:
|
||||
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 sys
|
||||
import inspect
|
||||
|
||||
|
||||
def as_bytearray(buffer):
|
||||
"""
|
||||
Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
|
||||
MicroPython friendly manner.
|
||||
"""
|
||||
ui8a = js.Uint8Array.new(buffer)
|
||||
size = ui8a.length
|
||||
ba = bytearray(size)
|
||||
@@ -31,3 +37,22 @@ class NotSupported:
|
||||
|
||||
def __call__(self, *args):
|
||||
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
|
||||
# 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):
|
||||
@@ -68,6 +69,18 @@ class Element:
|
||||
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`.
|
||||
self._classes = Classes(self)
|
||||
|
||||
@@ -75,7 +88,7 @@ class Element:
|
||||
self._style = Style(self)
|
||||
|
||||
# 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):
|
||||
"""Check for equality by comparing the underlying DOM element."""
|
||||
@@ -93,13 +106,21 @@ class Element:
|
||||
return self.find(key)
|
||||
|
||||
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
|
||||
# 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
|
||||
# Element instance via `for_`).
|
||||
if name.endswith("_"):
|
||||
name = name[:-1]
|
||||
|
||||
return getattr(self._dom_element, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
@@ -119,8 +140,33 @@ class Element:
|
||||
if name.endswith("_"):
|
||||
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)
|
||||
|
||||
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
|
||||
def children(self):
|
||||
"""Return the element's children as an `ElementCollection`."""
|
||||
|
||||
File diff suppressed because one or more lines are too long
14
core/tests/javascript/mpy-error.html
Normal file
14
core/tests/javascript/mpy-error.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!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>
|
||||
<script type="mpy">
|
||||
from pyscript import document
|
||||
import sys
|
||||
print("This is an error", file=sys.stderr)
|
||||
document.documentElement.classList.add("ok");
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
14
core/tests/javascript/mpy-no-error.html
Normal file
14
core/tests/javascript/mpy-no-error.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!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>
|
||||
<script type="mpy" config="mpy-no-error.toml">
|
||||
from pyscript import document
|
||||
import sys
|
||||
print("This is an error", file=sys.stderr)
|
||||
document.documentElement.classList.add("ok");
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
1
core/tests/javascript/mpy-no-error.toml
Normal file
1
core/tests/javascript/mpy-no-error.toml
Normal file
@@ -0,0 +1 @@
|
||||
plugins = ["!error"]
|
||||
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>
|
||||
@@ -138,3 +138,36 @@ test('Pyodide lockFileURL vs CDN', async ({ page }) => {
|
||||
const body = await page.evaluate(() => document.body.textContent);
|
||||
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 }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/mpy-error.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
const body = await page.evaluate(() => document.body.textContent.trim());
|
||||
await expect(body).toBe('This is an error');
|
||||
});
|
||||
|
||||
test('MicroPython buffered NO error', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/mpy-no-error.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
const body = await page.evaluate(() => document.body.textContent.trim());
|
||||
await expect(body).toBe('');
|
||||
});
|
||||
|
||||
@@ -12,13 +12,23 @@ const {
|
||||
kill,
|
||||
} = await donkey({ terminal: '#container' });
|
||||
|
||||
clearButton.onclick = clear;
|
||||
killButton.onclick = kill;
|
||||
clearButton.onclick = async () => {
|
||||
killButton.disabled = true;
|
||||
clearButton.disabled = true;
|
||||
await clear();
|
||||
runButton.disabled = false;
|
||||
};
|
||||
killButton.onclick = () => {
|
||||
killButton.disabled = true;
|
||||
clearButton.disabled = true;
|
||||
runButton.disabled = true;
|
||||
kill();
|
||||
};
|
||||
|
||||
runButton.disabled = false;
|
||||
runButton.onclick = async () => {
|
||||
killButton.disabled = false;
|
||||
clearButton.disabled = true;
|
||||
clearButton.disabled = false;
|
||||
runButton.disabled = true;
|
||||
// multiline code
|
||||
await execute(`
|
||||
@@ -29,6 +39,5 @@ runButton.onclick = async () => {
|
||||
const name = await evaluate('input("what is your name? ")');
|
||||
alert(`Hello ${name}`);
|
||||
killButton.disabled = true;
|
||||
clearButton.disabled = false;
|
||||
runButton.disabled = false;
|
||||
};
|
||||
|
||||
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, "]")
|
||||
39
core/tests/manual/error/index.html
Normal file
39
core/tests/manual/error/index.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript Error Bug?</title>
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<py-config>
|
||||
plugins = ["!error"]
|
||||
</py-config>
|
||||
<script type="py">
|
||||
import sys
|
||||
print("This is normal content")
|
||||
print("This is error content", file=sys.stderr)
|
||||
</script>
|
||||
<!-- Attempt 2; inline config
|
||||
<script type="py" config='plugins=["!error"]'>
|
||||
import sys
|
||||
print("This is normal content")
|
||||
print("This is error content", file=sys.stderr)
|
||||
</script> -->
|
||||
<!-- Attempt 3; external pyscript.toml
|
||||
<script type="py" config="pyscript.toml">
|
||||
import sys
|
||||
print("This is normal content")
|
||||
print("This is error content", file=sys.stderr)
|
||||
</script> -->
|
||||
<!-- Attempt 4; micropython
|
||||
<script type="mpy">
|
||||
import sys
|
||||
print("This is normal content")
|
||||
print("This is error content", file=sys.stderr)
|
||||
</script> -->
|
||||
<div id="result"></div>
|
||||
</body>
|
||||
</html>
|
||||
1
core/tests/manual/error/pyscript.toml
Normal file
1
core/tests/manual/error/pyscript.toml
Normal file
@@ -0,0 +1 @@
|
||||
plugins = ["!error"]
|
||||
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>
|
||||
13
core/tests/manual/issue-2228/index.html
Normal file
13
core/tests/manual/issue-2228/index.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">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" config='{"packages":["jsonpointer==3.0.0"]}'>
|
||||
print('Hello World')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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>I'm another button you can click</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>
|
||||
<p class="collection"></p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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_current_target.py": "tests/test_current_target.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_web.py": "tests/test_web.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"
|
||||
},
|
||||
"js_modules": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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_current_target.py": "tests/test_current_target.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_web.py": "tests/test_web.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"
|
||||
},
|
||||
"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()
|
||||
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):
|
||||
# GIVEN an existing element on the page with a known empty text content
|
||||
div = web.page.find("#element_attribute_tests")[0]
|
||||
@@ -227,11 +278,15 @@ class TestCollection:
|
||||
assert el.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):
|
||||
called = False
|
||||
call_flag = asyncio.Event()
|
||||
|
||||
buttons_collection = web.page.find("button")
|
||||
buttons_collection = web.page["button"]
|
||||
|
||||
@when("click", buttons_collection)
|
||||
def on_click(event):
|
||||
@@ -249,6 +304,28 @@ class TestCollection:
|
||||
called = False
|
||||
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:
|
||||
|
||||
@@ -759,14 +836,13 @@ class TestElements:
|
||||
self._create_el_and_basic_asserts("iframe", properties=properties)
|
||||
|
||||
@upytest.skip(
|
||||
"Flakey on Pyodide in worker.",
|
||||
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
|
||||
"Flakey in worker.",
|
||||
skip_when=RUNNING_IN_WORKER,
|
||||
)
|
||||
async def test_img(self):
|
||||
"""
|
||||
This test contains a bespoke version of the _create_el_and_basic_asserts
|
||||
function so we can await asyncio.sleep if in a worker, so the DOM state
|
||||
is in sync with the worker before property based asserts can happen.
|
||||
This test, thanks to downloading an image from the internet, is flakey
|
||||
when run in a worker. It's skipped when running in a worker.
|
||||
"""
|
||||
properties = {
|
||||
"src": "https://picsum.photos/600/400",
|
||||
@@ -774,39 +850,7 @@ class TestElements:
|
||||
"width": 250,
|
||||
"height": 200,
|
||||
}
|
||||
|
||||
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)}"
|
||||
self._create_el_and_basic_asserts("img", properties=properties)
|
||||
|
||||
def test_input(self):
|
||||
# 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 I: boolean;
|
||||
declare namespace r {
|
||||
declare var v: any;
|
||||
declare var k: boolean;
|
||||
declare namespace i {
|
||||
export let __esModule: boolean;
|
||||
export { Readline };
|
||||
}
|
||||
@@ -57,7 +57,7 @@ declare class Readline {
|
||||
highlighter: any;
|
||||
history: any;
|
||||
promptSize: any;
|
||||
layout: p;
|
||||
layout: c;
|
||||
buffer(): string;
|
||||
shouldHighlight(): boolean;
|
||||
clearScreen(): void;
|
||||
@@ -124,15 +124,15 @@ declare class Readline {
|
||||
readPaste(t: any): void;
|
||||
readKey(t: any): void;
|
||||
}
|
||||
declare class p {
|
||||
declare class c {
|
||||
constructor(t: any);
|
||||
promptSize: any;
|
||||
cursor: c;
|
||||
end: c;
|
||||
cursor: u;
|
||||
end: u;
|
||||
}
|
||||
declare class c {
|
||||
declare class u {
|
||||
constructor(t: any, e: any);
|
||||
row: 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 s: any;
|
||||
declare var t: {};
|
||||
export { i as Terminal, s as __esModule, t as default };
|
||||
declare var D: any;
|
||||
declare var R: any;
|
||||
declare var L: {};
|
||||
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 function relative_url(url: any, base?: string): string;
|
||||
|
||||
12
core/types/core.d.ts
vendored
12
core/types/core.d.ts
vendored
@@ -1,5 +1,13 @@
|
||||
export function donkey(options: any): Promise<any>;
|
||||
export function donkey(options: any): Promise<{
|
||||
process: (code: any) => Promise<any>;
|
||||
execute: (code: any) => Promise<any>;
|
||||
evaluate: (code: any) => Promise<any>;
|
||||
clear: () => Promise<void>;
|
||||
reset: () => Promise<void>;
|
||||
kill: () => void;
|
||||
}>;
|
||||
export function offline_interpreter(config: any): string;
|
||||
import codemirror from "./plugins/codemirror.js";
|
||||
import { stdlib } from "./stdlib.js";
|
||||
import { optional } from "./stdlib.js";
|
||||
import { inputFailure } from "./hooks.js";
|
||||
@@ -56,4 +64,4 @@ declare const exportedHooks: {
|
||||
};
|
||||
declare const exportedConfig: {};
|
||||
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: {
|
||||
codemirror: () => Promise<typeof import("./plugins/codemirror.js")>;
|
||||
"deprecations-manager": () => Promise<typeof import("./plugins/deprecations-manager.js")>;
|
||||
donkey: () => Promise<typeof import("./plugins/donkey.js")>;
|
||||
error: () => Promise<typeof import("./plugins/error.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")>;
|
||||
};
|
||||
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;
|
||||
9
core/types/plugins/donkey.d.ts
vendored
9
core/types/plugins/donkey.d.ts
vendored
@@ -1,2 +1,9 @@
|
||||
declare function _default(options?: {}): Promise<any>;
|
||||
declare function _default(options?: {}): Promise<{
|
||||
process: (code: any) => Promise<any>;
|
||||
execute: (code: any) => Promise<any>;
|
||||
evaluate: (code: any) => Promise<any>;
|
||||
clear: () => Promise<void>;
|
||||
reset: () => Promise<void>;
|
||||
kill: () => void;
|
||||
}>;
|
||||
export default _default;
|
||||
|
||||
1
core/types/plugins/error.d.ts
vendored
1
core/types/plugins/error.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
export function notOnDOM(): void;
|
||||
/**
|
||||
* Add a banner to the top of the page, notifying the user of an error
|
||||
* @param {string} message
|
||||
|
||||
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: {
|
||||
"__init__.py": string;
|
||||
"display.py": string;
|
||||
"event_handling.py": string;
|
||||
"events.py": string;
|
||||
"fetch.py": string;
|
||||
"ffi.py": string;
|
||||
"flatted.py": string;
|
||||
|
||||
@@ -39,6 +39,15 @@
|
||||
#header p {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
#header > svg {
|
||||
max-width: 480px;
|
||||
}
|
||||
@media only screen and (max-width: 400px) {
|
||||
#header > svg {
|
||||
width: 256px;
|
||||
height: 87px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function copyToClipboard() {
|
||||
@@ -53,12 +62,7 @@
|
||||
<body>
|
||||
<main>
|
||||
<div id="header">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="512"
|
||||
height="173"
|
||||
viewBox="0 0 512 173"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 173">
|
||||
<path
|
||||
fill="#fda703"
|
||||
d="M170.14 16.147c12.97-12.966 34.252-21.918 51.437-11.75c3.776 2.234 7.3 5.063 10.242 8.321c9.075 10.047 11.853 24.04 11.915 37.193c.125 26.222-9.355 56.093-34.555 68.782c-9.635 4.851-20.398 6.592-31.126 6.122c-6.042-.265-12.118-1.49-18.162-1.568l-.567-.004v46.425h-20.047V3.222h20.047v26.642h.264c2.19-5.078 6.687-9.854 10.551-13.717m1.43 20.312c-1.706 1.975-3.301 4.022-4.862 6.111l-1.033 1.386c-2.247 3-4.878 6.491-6.095 9.912c-.91 2.559-.256 6.27-.256 8.969v42.469l.845.173c13.804 2.808 27.723 4.557 40.305-3.454c17.28-11.002 22.71-32.58 21.39-51.85l-.04-.559c-.678-9.13-2.678-18.348-9.484-25.017c-13.438-13.167-31.313.912-40.77 11.86M261.67 3.222c.852 2.961 2.596 5.653 3.875 8.441c3.134 6.827 6.502 13.549 9.773 20.311c9.214 19.044 18.41 38.111 27.444 57.24c2.807 5.944 5.718 11.838 8.551 17.768l1.21 2.544c1.239 2.626 3.65 5.807 4.053 8.705c.196 1.397-.933 3.115-1.467 4.396l-.036.088a79.5 79.5 0 0 1-5.605 11.343l-.86 1.439c-1.961 3.273-4.013 6.592-6.603 9.376l-.392.42c-2.033 2.167-4.196 4.296-6.706 5.896c-11.047 7.047-23.326 5.408-35.347 2.652v17.674l.842.107c13.763 1.742 27.7 2.564 40.572-3.871c13.107-6.554 21-19.512 27.502-32.11c11.882-23.024 20.787-47.708 30.778-71.61l.39-.93C365.137 50.016 370.51 36.88 376 23.797c1.806-4.304 3.56-8.631 5.389-12.925c1.036-2.433 2.49-5.036 2.94-7.65h-14.243c-1.433 0-4.197-.528-5.437.271c-1.488.96-2.137 4.48-2.815 6.06c-2.363 5.496-4.505 11.088-6.787 16.618c-6.304 15.28-12.598 30.568-18.778 45.898c-2.72 6.742-6.71 13.77-8.389 20.839h-.527l-3.89-8.177l-6.857-13.98l-21.368-43.789l-8.31-16.882l-3.532-6.587l-5.898-.27zM0 64.683c2.398 1.987 5.918 3.156 8.705 4.55l71.748 35.874c7.172 3.586 14.3 7.25 21.46 10.856l3.072 1.542c3.547 1.773 7.697 4.777 11.606 5.474v-12.925l-.27-5.173l-6.06-3.334l-15.3-7.65l-40.358-20.31l-17.674-8.904l17.674-8.77l40.095-19.917l15.563-7.78l6.06-3.268l.27-5.108V6.651c-4.078.727-8.432 3.888-12.134 5.738L26.378 51.43C17.63 55.804 8.446 59.754 0 64.683M395.672 6.651v12.926c0 1.382-.496 3.972.272 5.172c.958 1.5 4.234 2.423 5.795 3.203l15.036 7.517c13.292 6.646 26.536 13.4 39.83 20.047c5.92 2.96 12.1 7.333 18.466 9.167v.528c-6.835 1.969-13.68 6.511-20.048 9.695l-38.512 19.256c-4.635 2.317-9.224 4.866-13.98 6.923l-.184.078c-1.805.75-5.401 1.885-6.403 3.454c-.768 1.2-.272 3.79-.272 5.173v13.189l14.508-7.057l77.025-38.512L512 64.947c-2.654-2.68-7.705-4.182-11.079-5.868L404.905 11.07c-2.965-1.483-6.044-3.5-9.233-4.419"
|
||||
|
||||
Reference in New Issue
Block a user