Compare commits

...

22 Commits

Author SHA1 Message Date
webreflection
c0fcfa7a07 WIP - PyGame-CE - main thread only so far 2024-12-10 15:53:49 +01:00
Andrea Giammarchi
d143b229ed Added __terminal__ in non-persistent donkey (#2260) 2024-12-06 14:53:09 +01:00
Andrea Giammarchi
0d74a60227 Fix #2257 - Updated polyscript (#2258) 2024-12-03 20:33:25 +01:00
Andrea Giammarchi
ce923a354f Updated to latest Pyodide + cleaner Polyscript (#2256) 2024-11-27 13:25:37 +01:00
Andrea Giammarchi
7e65836423 Factored out codemirror as chunked lazy based import (#2252)
* Factored out codemirror as chunked lazy based import
2024-11-26 14:17:07 +01:00
Andrea Giammarchi
796373cfa6 Fix #2246 - Override builtins.input to avoid duplicating it (#2254) 2024-11-25 14:17:30 +01:00
Andrea Giammarchi
0a1d3bb678 Fix #2245 - Filter all versions w/ Pyodide frozen cache (#2251) 2024-11-19 11:47:47 +01:00
Andrea Giammarchi
4e43d3e92d Fix #2242 - Improved Xterm.js glyphs handling (#2248)
* Updated dev/dependencies

* Fix #2242 - Improved Xterm.js glyphs handling
2024-11-14 10:28:38 +01:00
Andrea Giammarchi
5acc2afaf3 WIP: Xterm update (#2237)
* WIP: Xterm Update
2024-11-12 10:21:36 +01:00
Nicholas Tollervey
56c64cbee7 Refactor @when and add Event (#2239)
* Add two unit tests for illustrative purposes.

* Radical simplification of @when, more tests and some minor refactoring.
Handle ElementCollections, tests for ElementCollection, make serve for running tests locally.

* Skip flakey Pyodide in worker test (it works 50/50 and appears to be a timing issue).

* Ensure onFOO relates to an underlying FOO event in an Element.

* Minor comment cleanup.

* Add async test for Event listeners.

* Handlers no longer require an event parameter.

* Add tests for async handling via when.

* Docstring cleanup.

* Refactor onFOO to on_FOO.

* Minor typo tidy ups.

* Use correct check for MicroPython.

---------

Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-11-05 13:55:28 +00:00
Andrea Giammarchi
4ff02a24d1 Fix #2240 - Update polyscript to allow targeting /* multiple times (#2241) 2024-11-05 14:20:26 +01:00
Andrea Giammarchi
a5dc94792b Fixed MicroPython error on non-worker terminal (#2238) 2024-10-30 16:08:50 +01:00
Andrea Giammarchi
0db79e0f02 Fix #2065 - Allow usage of index_urls for Pyodide (#2235) 2024-10-30 10:34:45 +01:00
Andrea Giammarchi
283eabdb30 Added pinned lockFileURL test (#2234) 2024-10-29 11:12:21 +01:00
Nicholas Tollervey
3f19e00410 Update the size of the Py icon on the release page template (#2232)
* Update the size of the Py icon on the release page template, so it works in a mobile friendly manner.

* Use CSS with media for responsive icon.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-28 11:00:51 +00:00
Andrea Giammarchi
9233d5e45a Fix #2220 - Delay plugins resolution due Safari 17.6 greedy resolution (#2229)
* Fix #2220 - Delay plugins resolution due Safari 17.6 greedy resolution

* Fix #2228 - Workaround in Polyscript for lockFileURL
2024-10-28 11:00:23 +01:00
Nicholas Tollervey
fe580cd90b Ensure precommit check is done before each build when using make. (#2231) 2024-10-24 15:27:29 +01:00
Nicholas Tollervey
00e6cfed29 Fix Makefile so JS dependencies don't get into a bad state. (#2230) 2024-10-24 11:13:02 +01:00
Andrea Giammarchi
6b1330d28a Fix #2220 - Avoid DOM notifications on errors (#2226)
* Fix #2220 - Avoid DOM notifications on errors
2024-10-17 16:18:05 +02:00
Carlovo
5d751493f6 Fix typo in README.md (#2219)
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-10-15 12:57:28 +02:00
Andrea Giammarchi
c3517f7973 Donkey clear and reset now terminate when busy (#2225)
* Donkey clear and reset now terminate when busy
2024-10-15 12:56:11 +02:00
Andrea Giammarchi
b1c33b7f79 Kill previous worker if another eval/execute is asked to the donkey (#2218)
Kill previous worker if another eval/execute is asked to the donkey
2024-10-11 16:12:56 +02:00
60 changed files with 2071 additions and 807 deletions

View File

@@ -40,7 +40,7 @@ check-python:
# Check the environment, install the dependencies. # Check the environment, install the dependencies.
setup: check-node check-npm check-python setup: check-node check-npm check-python
cd core && npm install && cd .. cd core && npm ci && cd ..
ifeq ($(VIRTUAL_ENV),) ifeq ($(VIRTUAL_ENV),)
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m" echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
false false
@@ -55,12 +55,11 @@ clean:
rm -rf .pytest_cache .coverage coverage.xml rm -rf .pytest_cache .coverage coverage.xml
# Build PyScript. # Build PyScript.
build: build: precommit-check
cd core && npx playwright install chromium && npm run build cd core && npx playwright install chromium && npm run build
# Update the dependencies. # Update the dependencies.
update: update:
cd core && npm update && cd ..
python -m pip install -r requirements.txt --upgrade python -m pip install -r requirements.txt --upgrade
# Run the precommit checks (run eslint). # Run the precommit checks (run eslint).
@@ -71,6 +70,10 @@ precommit-check:
test: test:
cd core && npm run test:integration cd core && npm run test:integration
# Serve the repository with the correct headers.
serve:
npx mini-coi .
# Format the code. # Format the code.
fmt: fmt-py fmt: fmt-py
@echo "Format completed" @echo "Format completed"

View File

@@ -1,6 +1,6 @@
# PyScript # 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: 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, to learn about our development process, reporting bugs and improvements,
creating issues and asking questions. creating issues and asking questions.
Check out the [developing process](https://docs.pyscript.net/latest/developers/) Check out the [development process](https://docs.pyscript.net/latest/developers/)
documentation for more information on how to setup your development environment. documentation for more information on how to setup your development environment.
## Governance ## Governance

700
core/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.6.4", "version": "0.6.22",
"type": "module", "type": "module",
"description": "PyScript", "description": "PyScript",
"module": "./index.js", "module": "./index.js",
@@ -60,40 +60,41 @@
"dependencies": { "dependencies": {
"@ungap/with-resolvers": "^0.1.0", "@ungap/with-resolvers": "^0.1.0",
"@webreflection/idb-map": "^0.3.2", "@webreflection/idb-map": "^0.3.2",
"add-promise-listener": "^0.1.3",
"basic-devtools": "^0.1.6", "basic-devtools": "^0.1.6",
"polyscript": "^0.16.2", "polyscript": "^0.16.10",
"sabayon": "^0.5.2", "sabayon": "^0.6.1",
"sticky-module": "^0.1.1", "sticky-module": "^0.1.1",
"to-json-callback": "^0.1.1", "to-json-callback": "^0.1.1",
"type-checked-collections": "^0.1.7" "type-checked-collections": "^0.1.7"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/commands": "^6.7.0", "@codemirror/commands": "^6.7.1",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
"@codemirror/language": "^6.10.3", "@codemirror/language": "^6.10.6",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.34.1", "@codemirror/view": "^6.35.0",
"@playwright/test": "1.45.3", "@playwright/test": "1.45.3",
"@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@webreflection/toml-j0.4": "^1.1.3", "@webreflection/toml-j0.4": "^1.1.3",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
"bun": "^1.1.30", "@xterm/xterm": "^5.5.0",
"bun": "^1.1.38",
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"codedent": "^0.1.2", "codedent": "^0.1.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"eslint": "^9.12.0", "eslint": "^9.16.0",
"flatted": "^3.3.1", "flatted": "^3.3.2",
"rollup": "^4.24.0", "rollup": "^4.28.1",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",
"static-handler": "^0.5.3", "static-handler": "^0.5.3",
"string-width": "^7.2.0", "string-width": "^7.2.0",
"typescript": "^5.6.3", "typescript": "^5.7.2",
"xterm": "^5.3.0", "xterm-readline": "^1.1.2"
"xterm-readline": "^1.1.1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -46,7 +46,7 @@ const modules = {
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"), "toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
// xterm // xterm
"xterm.js": resolve("xterm"), "xterm.js": resolve("@xterm/xterm"),
"xterm-readline.js": resolve("xterm-readline"), "xterm-readline.js": resolve("xterm-readline"),
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) => "xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
b.text(), b.text(),
@@ -54,9 +54,9 @@ const modules = {
"xterm_addon-web-links.js": fetch( "xterm_addon-web-links.js": fetch(
`${CDN}/@xterm/addon-web-links/+esm`, `${CDN}/@xterm/addon-web-links/+esm`,
).then((b) => b.text()), ).then((b) => b.text()),
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then( "xterm.css": fetch(
(b) => b.text(), `${CDN}/@xterm/xterm@${v("@xterm/xterm")}/css/xterm.min.css`,
), ).then((b) => b.text()),
// codemirror // codemirror
"codemirror.js": reBundle("codemirror"), "codemirror.js": reBundle("codemirror"),

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
/** /**
* Minified by jsDelivr using clean-css v5.3.2. * Minified by jsDelivr using clean-css v5.3.2.
* Original file: /npm/xterm@5.3.0/css/xterm.css * Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
* *
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/ */
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative} .xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}

File diff suppressed because one or more lines are too long

View File

@@ -56,7 +56,7 @@ const syntaxError = (type, url, { message }) => {
const configs = new Map(); const configs = new Map();
for (const [TYPE] of TYPES) { for (const [TYPE] of TYPES) {
/** @type {Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */ /** @type {() => Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
let plugins; let plugins;
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */ /** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
@@ -135,6 +135,7 @@ for (const [TYPE] of TYPES) {
// parse all plugins and optionally ignore only // parse all plugins and optionally ignore only
// those flagged as "undesired" via `!` prefix // those flagged as "undesired" via `!` prefix
plugins = async () => {
const toBeAwaited = []; const toBeAwaited = [];
for (const [key, value] of Object.entries(allPlugins)) { for (const [key, value] of Object.entries(allPlugins)) {
if (error) { if (error) {
@@ -146,11 +147,12 @@ for (const [TYPE] of TYPES) {
} }
} else if (!parsed?.plugins?.includes(`!${key}`)) { } else if (!parsed?.plugins?.includes(`!${key}`)) {
toBeAwaited.push(value().then(({ default: p }) => p)); toBeAwaited.push(value().then(({ default: p }) => p));
} else if (key === "error") {
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
} }
} }
return await Promise.all(toBeAwaited);
// assign plugins as Promise.all only if needed };
plugins = Promise.all(toBeAwaited);
configs.set(TYPE, { config: parsed, configURL, plugins, error }); configs.set(TYPE, { config: parsed, configURL, plugins, error });
} }

View File

@@ -73,3 +73,8 @@ mpy-config {
background-color: #fff; background-color: #fff;
animation: spinner 0.6s linear infinite; animation: spinner 0.6s linear infinite;
} }
py-terminal span,
mpy-terminal span {
letter-spacing: 0 !important;
}

View File

@@ -34,6 +34,9 @@ import {
inputFailure, inputFailure,
} from "./hooks.js"; } from "./hooks.js";
import codemirror from "./plugins/codemirror.js";
export { codemirror };
import { stdlib, optional } from "./stdlib.js"; import { stdlib, optional } from "./stdlib.js";
export { stdlib, optional, inputFailure }; export { stdlib, optional, inputFailure };
@@ -179,7 +182,7 @@ for (const [TYPE, interpreter] of TYPES) {
// ensure plugins are bootstrapped already before custom type definition // ensure plugins are bootstrapped already before custom type definition
// NOTE: we cannot top-level await in here as plugins import other utilities // NOTE: we cannot top-level await in here as plugins import other utilities
// from core.js itself so that custom definition should not be blocking. // from core.js itself so that custom definition should not be blocking.
plugins.then(() => { plugins().then(() => {
// possible early errors sent by polyscript // possible early errors sent by polyscript
const errors = new Map(); const errors = new Map();
@@ -220,6 +223,13 @@ for (const [TYPE, interpreter] of TYPES) {
else element.after(show); else element.after(show);
} }
if (!show.id) show.id = getID(); if (!show.id) show.id = getID();
if (TYPE === "py") {
const canvas2D = element.getAttribute("canvas2d") || element.getAttribute("canvas");
if (canvas2D) {
const canvas = queryTarget(document, canvas2D);
wrap.interpreter.canvas.setCanvas2D(canvas);
}
}
// allows the code to retrieve the target element via // allows the code to retrieve the target element via
// document.currentScript.target if needed // document.currentScript.target if needed

View File

@@ -84,7 +84,19 @@ export const hooks = {
}, },
worker: { worker: {
/** @type {Set<function>} */ /** @type {Set<function>} */
onReady: new SetFunction(), onReady: new SetFunction([
(wrap, xworker) => {
if (wrap.type === "py") {
const { interpreter } = wrap;
const element = wrap.run('from polyscript import currentScript;currentScript');
const canvas2D = element.getAttribute("canvas2d") || element.getAttribute("canvas");
if (canvas2D) {
const canvas = element.ownerDocument.getElementById(canvas2D);
interpreter.canvas.setCanvas2D(canvas);
}
}
}
]),
/** @type {Set<function>} */ /** @type {Set<function>} */
onBeforeRun: new SetFunction(), onBeforeRun: new SetFunction(),
/** @type {Set<function>} */ /** @type {Set<function>} */

View File

@@ -1,5 +1,10 @@
// ⚠️ This file is an artifact: DO NOT MODIFY // ⚠️ This file is an artifact: DO NOT MODIFY
export default { export default {
codemirror: () =>
import(
/* webpackIgnore: true */
"./plugins/codemirror.js"
),
["deprecations-manager"]: () => ["deprecations-manager"]: () =>
import( import(
/* webpackIgnore: true */ /* webpackIgnore: true */

View 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"
);
},
};

View File

@@ -1,6 +1,6 @@
// PyScript Derepcations Plugin // PyScript Derepcations Plugin
import { hooks } from "../core.js";
import { notify } from "./error.js"; import { notify } from "./error.js";
import { hooks } from "../core.js";
// react lazily on PyScript bootstrap // react lazily on PyScript bootstrap
hooks.main.onReady.add(checkDeprecations); hooks.main.onReady.add(checkDeprecations);

View File

@@ -1,64 +1,114 @@
import addPromiseListener from "add-promise-listener";
import { assign, dedent } from "polyscript/exports"; import { assign, dedent } from "polyscript/exports";
const { stringify } = JSON;
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`; const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
export default (options = {}) => { const donkey = ({ type = "py", persistent, terminal, config }) => {
const type = options.type || "py"; const globals = terminal ? '{"__terminal__":__terminal__}' : "{}";
const args = options.persistent const args = persistent ? ["globals()", "__locals__"] : [globals, "{}"];
? ["globals()", "__locals__"]
: ["{}", "{}"];
const src = URL.createObjectURL( const src = URL.createObjectURL(
new Blob([ new Blob([
dedent(` [
from pyscript import sync, config // this array is to better minify this code once in production
__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}" "from pyscript import sync, config",
__locals__ = {} '__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"',
if config["type"] == "py": "__locals__ = {}",
import sys 'if config["type"] == "py":',
def __error__(_): " import sys",
info = sys.exc_info() " def __error__(_):",
return __message__(info[0], info[1]) " info = sys.exc_info()",
else: " return __message__(info[0], info[1])",
__error__ = lambda e: __message__(e.__class__, e.value) "else:",
def execute(code): " __error__ = lambda e: __message__(e.__class__, e.value)",
try: return ${invoke("exec", args)}; "def execute(code):",
except Exception as e: print(__error__(e)); ` try: return ${invoke("exec", args)};`,
def evaluate(code): " except Exception as e: print(__error__(e));",
try: return ${invoke("eval", args)}; "def evaluate(code):",
except Exception as e: print(__error__(e)); ` try: return ${invoke("eval", args)};`,
sync.execute = execute " except Exception as e: print(__error__(e));",
sync.evaluate = evaluate "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 }); const script = assign(document.createElement("script"), { type, src });
script.toggleAttribute("worker", true); script.toggleAttribute("worker", true);
script.toggleAttribute("terminal", true); script.toggleAttribute("terminal", true);
if (options.terminal) script.setAttribute("target", options.terminal); if (terminal) script.setAttribute("target", terminal);
if (options.config) if (config) {
script.setAttribute("config", JSON.stringify(options.config)); script.setAttribute(
"config",
typeof config === "string" ? config : stringify(config),
);
}
return new Promise((resolve) => { return addPromiseListener(
script.addEventListener(`${type}:done`, (event) => { document.body.appendChild(script),
event.stopPropagation(); `${type}:done`,
{ stopPropagation: true },
).then(() => {
URL.revokeObjectURL(src); URL.revokeObjectURL(src);
return script;
});
};
const utils = async (options) => {
const script = await donkey(options);
const { xworker, process, terminal } = script; const { xworker, process, terminal } = script;
const { execute, evaluate } = xworker.sync; const { execute, evaluate } = xworker.sync;
script.remove(); script.remove();
resolve({ return {
xworker,
process, process,
execute: (code) => execute(dedent(code)), terminal,
evaluate: (code) => evaluate(dedent(code)), execute,
clear: () => terminal.clear(), evaluate,
reset: () => terminal.reset(), };
kill: () => { };
xworker.terminate();
terminal.dispose(); export default async (options = {}) => {
}, let farmer = await utils(options);
}); let working = false;
}); const kill = () => {
document.body.append(script); 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,
};
}; };

View File

@@ -1,6 +1,12 @@
// PyScript Error Plugin // PyScript Error Plugin
import { buffered } from "polyscript/exports";
import { hooks } from "../core.js"; import { hooks } from "../core.js";
let dontBotherDOM = false;
export function notOnDOM() {
dontBotherDOM = true;
}
hooks.main.onReady.add(function override(pyScript) { hooks.main.onReady.add(function override(pyScript) {
// be sure this override happens only once // be sure this override happens only once
hooks.main.onReady.delete(override); hooks.main.onReady.delete(override);
@@ -8,13 +14,15 @@ hooks.main.onReady.add(function override(pyScript) {
// trap generic `stderr` to propagate to it regardless // trap generic `stderr` to propagate to it regardless
const { stderr } = pyScript.io; const { stderr } = pyScript.io;
// override it with our own logic const cb = (error, ...rest) => {
pyScript.io.stderr = (error, ...rest) => {
notify(error.message || error); notify(error.message || error);
// let other plugins or stderr hook, if any, do the rest // let other plugins or stderr hook, if any, do the rest
return stderr(error, ...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 // be sure uncaught Python errors are also visible
addEventListener("error", ({ message }) => { addEventListener("error", ({ message }) => {
if (message.startsWith("Uncaught PythonError")) notify(message); if (message.startsWith("Uncaught PythonError")) notify(message);
@@ -30,6 +38,7 @@ hooks.main.onReady.add(function override(pyScript) {
* @param {string} message * @param {string} message
*/ */
export function notify(message) { export function notify(message) {
if (dontBotherDOM) return;
const div = document.createElement("div"); const div = document.createElement("div");
div.className = "py-error"; div.className = "py-error";
div.textContent = message; div.textContent = message;

View File

@@ -2,6 +2,7 @@
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports"; import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js"; import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
import { notify } from "./error.js"; import { notify } from "./error.js";
import codemirror from "./codemirror.js";
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`; const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
@@ -194,14 +195,12 @@ const init = async (script, type, interpreter) => {
{ keymap }, { keymap },
{ defaultKeymap, indentWithTab }, { defaultKeymap, indentWithTab },
] = await Promise.all([ ] = await Promise.all([
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"), codemirror.core,
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"), codemirror.state,
import( codemirror.python,
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js" codemirror.language,
), codemirror.view,
import(/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"), codemirror.commands,
import(/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"),
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
]); ]);
let isSetup = script.hasAttribute("setup"); let isSetup = script.hasAttribute("setup");

View File

@@ -1,6 +1,6 @@
// PyScript pyodide terminal plugin // PyScript pyodide terminal plugin
import { hooks, inputFailure } from "../../core.js";
import { defineProperties } from "polyscript/exports"; import { defineProperties } from "polyscript/exports";
import { hooks, inputFailure } from "../../core.js";
const bootstrapped = new WeakSet(); const bootstrapped = new WeakSet();
@@ -34,6 +34,8 @@ const workerReady = ({ interpreter, io, run, type }, { sync }) => {
pyterminal_write(String(error.message || error)); pyterminal_write(String(error.message || error));
}; };
sync.pyterminal_stream_write = () => {};
// tiny shim of the code module with only interact // tiny shim of the code module with only interact
// to bootstrap a REPL like environment // to bootstrap a REPL like environment
interpreter.registerJsModule("code", { interpreter.registerJsModule("code", {
@@ -71,6 +73,7 @@ export default async (element) => {
disableStdin: false, disableStdin: false,
cursorBlink: true, cursorBlink: true,
cursorStyle: "block", cursorStyle: "block",
lineHeight: 1.2,
}; };
let stream; let stream;

View File

@@ -1,6 +1,6 @@
// PyScript py-terminal plugin // PyScript py-terminal plugin
import { hooks } from "../../core.js";
import { defineProperties } from "polyscript/exports"; import { defineProperties } from "polyscript/exports";
import { hooks } from "../../core.js";
const bootstrapped = new WeakSet(); const bootstrapped = new WeakSet();
@@ -126,6 +126,7 @@ export default async (element) => {
disableStdin: false, disableStdin: false,
cursorBlink: true, cursorBlink: true,
cursorStyle: "block", cursorStyle: "block",
lineHeight: 1.2,
}); });
xworker.sync.is_pyterminal = () => true; xworker.sync.is_pyterminal = () => true;
@@ -136,6 +137,18 @@ export default async (element) => {
// setup remote thread JS/Python code for whenever the // setup remote thread JS/Python code for whenever the
// worker is ready to become a terminal // worker is ready to become a terminal
hooks.worker.onReady.add(workerReady); hooks.worker.onReady.add(workerReady);
// @see https://github.com/pyscript/pyscript/issues/2246
const patchInput = [
"import builtins as _b",
"from pyscript import sync as _s",
"_b.input = _s.pyterminal_read",
"del _b",
"del _s",
].join("\n");
hooks.worker.codeBeforeRun.add(patchInput);
hooks.worker.codeBeforeRunAsync.add(patchInput);
} else { } else {
// in the main case, just bootstrap XTerm without // in the main case, just bootstrap XTerm without
// allowing any input as that's not possible / awkward // allowing any input as that's not possible / awkward

File diff suppressed because one or more lines are too long

View File

@@ -30,8 +30,6 @@
# as it works transparently in both the main thread and worker cases. # as it works transparently in both the main thread and worker cases.
from polyscript import lazy_py_modules as py_import from polyscript import lazy_py_modules as py_import
from pyscript.display import HTML, display
from pyscript.fetch import fetch
from pyscript.magic_js import ( from pyscript.magic_js import (
RUNNING_IN_WORKER, RUNNING_IN_WORKER,
PyWorker, PyWorker,
@@ -43,19 +41,11 @@ from pyscript.magic_js import (
sync, sync,
window, window,
) )
from pyscript.display import HTML, display
from pyscript.fetch import fetch
from pyscript.storage import Storage, storage from pyscript.storage import Storage, storage
from pyscript.websocket import WebSocket from pyscript.websocket import WebSocket
from pyscript.events import when, Event
if not RUNNING_IN_WORKER: if not RUNNING_IN_WORKER:
from pyscript.workers import create_named_worker, workers from pyscript.workers import create_named_worker, workers
try:
from pyscript.event_handling import when
except:
# TODO: should we remove this? Or at the very least, we should capture
# the traceback otherwise it's very hard to debug
from pyscript.util import NotSupported
when = NotSupported(
"pyscript.when", "pyscript.when currently not available with this interpreter"
)

View File

@@ -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

View 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

View File

@@ -45,6 +45,8 @@ if RUNNING_IN_WORKER:
window = polyscript.xworker.window window = polyscript.xworker.window
document = window.document document = window.document
# weird + not worth it as it does not work anyway
js.screen = window.screen
js.document = document js.document = document
# this is the same as js_import on main and it lands modules on main # this is the same as js_import on main and it lands modules on main
js_import = window.Function( js_import = window.Function(

View File

@@ -1,7 +1,13 @@
import js import js
import sys
import inspect
def as_bytearray(buffer): def as_bytearray(buffer):
"""
Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
MicroPython friendly manner.
"""
ui8a = js.Uint8Array.new(buffer) ui8a = js.Uint8Array.new(buffer)
size = ui8a.length size = ui8a.length
ba = bytearray(size) ba = bytearray(size)
@@ -31,3 +37,22 @@ class NotSupported:
def __call__(self, *args): def __call__(self, *args):
raise TypeError(self.error) raise TypeError(self.error)
def is_awaitable(obj):
"""
Returns a boolean indication if the passed in obj is an awaitable
function. (MicroPython treats awaitables as generator functions, and if
the object is a closure containing an async function we need to work
carefully.)
"""
from pyscript import config
if config["type"] == "mpy": # Is MicroPython?
# MicroPython doesn't appear to have a way to determine if a closure is
# an async function except via the repr. This is a bit hacky.
if "<closure <generator>" in repr(obj):
return True
return inspect.isgeneratorfunction(obj)
return inspect.iscoroutinefunction(obj)

View File

@@ -2,7 +2,8 @@
# `when` is not used in this module. It is imported here save the user an additional # `when` is not used in this module. It is imported here save the user an additional
# import (i.e. they can get what they need from `pyscript.web`). # import (i.e. they can get what they need from `pyscript.web`).
from pyscript import document, when # NOQA from pyscript import document, when, Event # NOQA
from pyscript.ffi import create_proxy
def wrap_dom_element(dom_element): def wrap_dom_element(dom_element):
@@ -68,6 +69,18 @@ class Element:
type(self).get_tag_name() type(self).get_tag_name()
) )
# HTML on_events attached to the element become pyscript.Event instances.
self._on_events = {}
# Handle kwargs for handling named events with a default handler function.
properties = {}
for name, handler in kwargs.items():
if name.startswith("on_"):
ev = self.get_event(name) # Create the default Event instance.
ev.add_listener(handler)
else:
properties[name] = handler
# A set-like interface to the element's `classList`. # A set-like interface to the element's `classList`.
self._classes = Classes(self) self._classes = Classes(self)
@@ -75,7 +88,7 @@ class Element:
self._style = Style(self) self._style = Style(self)
# Set any specified classes, styles, and DOM properties. # Set any specified classes, styles, and DOM properties.
self.update(classes=classes, style=style, **kwargs) self.update(classes=classes, style=style, **properties)
def __eq__(self, obj): def __eq__(self, obj):
"""Check for equality by comparing the underlying DOM element.""" """Check for equality by comparing the underlying DOM element."""
@@ -93,13 +106,21 @@ class Element:
return self.find(key) return self.find(key)
def __getattr__(self, name): def __getattr__(self, name):
"""
Get an attribute from the element.
If the attribute is an event (e.g. "on_click"), we wrap it in an `Event`
instance and return that. Otherwise, we return the attribute from the
underlying DOM element.
"""
if name.startswith("on_"):
return self.get_event(name)
# This allows us to get attributes on the underlying DOM element that clash # This allows us to get attributes on the underlying DOM element that clash
# with Python keywords or built-ins (e.g. the output element has an # with Python keywords or built-ins (e.g. the output element has an
# attribute `for` which is a Python keyword, so you can access it on the # attribute `for` which is a Python keyword, so you can access it on the
# Element instance via `for_`). # Element instance via `for_`).
if name.endswith("_"): if name.endswith("_"):
name = name[:-1] name = name[:-1]
return getattr(self._dom_element, name) return getattr(self._dom_element, name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
@@ -119,8 +140,33 @@ class Element:
if name.endswith("_"): if name.endswith("_"):
name = name[:-1] name = name[:-1]
if name.startswith("on_"):
# Ensure on-events are cached in the _on_events dict if the
# user is setting them directly.
self._on_events[name] = value
setattr(self._dom_element, name, value) setattr(self._dom_element, name, value)
def get_event(self, name):
"""
Get an `Event` instance for the specified event name.
"""
if not name.startswith("on_"):
raise ValueError("Event names must start with 'on_'.")
event_name = name[3:] # Remove the "on_" prefix.
if not hasattr(self._dom_element, event_name):
raise ValueError(f"Element has no '{event_name}' event.")
if name in self._on_events:
return self._on_events[name]
# Such an on-event exists in the DOM element, but we haven't yet
# wrapped it in an Event instance. Let's do that now. When the
# underlying DOM element's event is triggered, the Event instance
# will be triggered too.
ev = Event()
self._on_events[name] = ev
self._dom_element.addEventListener(event_name, create_proxy(ev.trigger))
return ev
@property @property
def children(self): def children(self):
"""Return the element's children as an `ElementCollection`.""" """Return the element's children as an `ElementCollection`."""

File diff suppressed because one or more lines are too long

View 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>

View 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>

View File

@@ -0,0 +1 @@
plugins = ["!error"]

View 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>

View File

@@ -138,3 +138,36 @@ test('Pyodide lockFileURL vs CDN', async ({ page }) => {
const body = await page.evaluate(() => document.body.textContent); const body = await page.evaluate(() => document.body.textContent);
await expect(body).toBe('OK'); await expect(body).toBe('OK');
}); });
test('Pyodide pinned lockFileURL', async ({ page }) => {
const logs = [];
page.on('console', msg => {
const text = msg.text();
if (!text.startsWith('['))
logs.push(text);
});
await page.goto('http://localhost:8080/tests/javascript/pyodide-lockfile/');
await page.waitForSelector('html.done');
let body = await page.evaluate(() => document.body.lastChild.textContent);
await expect(body).toBe('OK');
await expect(!!logs.splice(0).length).toBe(true);
await page.reload();
await page.waitForSelector('html.done');
body = await page.evaluate(() => document.body.lastChild.textContent);
await expect(body).toBe('OK');
await expect(logs.splice(0).length).toBe(0);
});
test('MicroPython buffered error', async ({ page }) => {
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('');
});

View File

@@ -12,13 +12,23 @@ const {
kill, kill,
} = await donkey({ terminal: '#container' }); } = await donkey({ terminal: '#container' });
clearButton.onclick = clear; clearButton.onclick = async () => {
killButton.onclick = kill; 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.disabled = false;
runButton.onclick = async () => { runButton.onclick = async () => {
killButton.disabled = false; killButton.disabled = false;
clearButton.disabled = true; clearButton.disabled = false;
runButton.disabled = true; runButton.disabled = true;
// multiline code // multiline code
await execute(` await execute(`
@@ -29,6 +39,5 @@ runButton.onclick = async () => {
const name = await evaluate('input("what is your name? ")'); const name = await evaluate('input("what is your name? ")');
alert(`Hello ${name}`); alert(`Hello ${name}`);
killButton.disabled = true; killButton.disabled = true;
clearButton.disabled = false;
runButton.disabled = false; runButton.disabled = false;
}; };

View 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>

View 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, "]")

View 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>

View File

@@ -0,0 +1 @@
plugins = ["!error"]

View 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;
}

View File

@@ -0,0 +1,394 @@
""" (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 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()

View File

@@ -0,0 +1,24 @@
<!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"
src="aliens.py"
canvas="canvas"
config='{"packages":["pygame-ce"]}'
></script>
<div class="demo">
<div class="demo-header">pygame.examples.aliens</div>
<div class="demo-content">
<canvas id="canvas"></canvas>
</div>
</div>
</body>
</html>

View 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>

View 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>

View File

@@ -0,0 +1 @@
print(input("What food would you like me to get from the shop? "))

View File

@@ -62,6 +62,7 @@
<button id="a-test-button">I'm a button to be clicked</button> <button id="a-test-button">I'm a button to be clicked</button>
<button>I'm another button you can click</button> <button>I'm another button you can click</button>
<button id="a-third-button">2 is better than 3 :)</button> <button id="a-third-button">2 is better than 3 :)</button>
<button id="another-test-button">I'm another button to be clicked</button>
<div id="element-append-tests"></div> <div id="element-append-tests"></div>
<p class="collection"></p> <p class="collection"></p>

View File

@@ -1,6 +1,6 @@
{ {
"files": { "files": {
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/upytest.py": "", "https://raw.githubusercontent.com/ntoll/upytest/1.0.9/upytest.py": "",
"./tests/test_config.py": "tests/test_config.py", "./tests/test_config.py": "tests/test_config.py",
"./tests/test_current_target.py": "tests/test_current_target.py", "./tests/test_current_target.py": "tests/test_current_target.py",
"./tests/test_display.py": "tests/test_display.py", "./tests/test_display.py": "tests/test_display.py",
@@ -12,7 +12,7 @@
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py", "./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
"./tests/test_web.py": "tests/test_web.py", "./tests/test_web.py": "tests/test_web.py",
"./tests/test_websocket.py": "tests/test_websocket.py", "./tests/test_websocket.py": "tests/test_websocket.py",
"./tests/test_when.py": "tests/test_when.py", "./tests/test_events.py": "tests/test_events.py",
"./tests/test_window.py": "tests/test_window.py" "./tests/test_window.py": "tests/test_window.py"
}, },
"js_modules": { "js_modules": {

View File

@@ -1,6 +1,6 @@
{ {
"files": { "files": {
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/upytest.py": "", "https://raw.githubusercontent.com/ntoll/upytest/1.0.9/upytest.py": "",
"./tests/test_config.py": "tests/test_config.py", "./tests/test_config.py": "tests/test_config.py",
"./tests/test_current_target.py": "tests/test_current_target.py", "./tests/test_current_target.py": "tests/test_current_target.py",
"./tests/test_display.py": "tests/test_display.py", "./tests/test_display.py": "tests/test_display.py",
@@ -12,7 +12,7 @@
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py", "./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
"./tests/test_web.py": "tests/test_web.py", "./tests/test_web.py": "tests/test_web.py",
"./tests/test_websocket.py": "tests/test_websocket.py", "./tests/test_websocket.py": "tests/test_websocket.py",
"./tests/test_when.py": "tests/test_when.py", "./tests/test_events.py": "tests/test_events.py",
"./tests/test_window.py": "tests/test_window.py" "./tests/test_window.py": "tests/test_window.py"
}, },
"js_modules": { "js_modules": {

View 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

View 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)

View File

@@ -164,6 +164,57 @@ class TestElement:
await call_flag.wait() await call_flag.wait()
assert called assert called
async def test_when_decorator_on_event(self):
called = False
another_button = web.page.find("#another-test-button")[0]
call_flag = asyncio.Event()
assert another_button.on_click is not None
assert isinstance(another_button.on_click, web.Event)
@when(another_button.on_click)
def on_click(event):
nonlocal called
called = True
call_flag.set()
# Now let's simulate a click on the button (using the low level JS API)
# so we don't risk dom getting in the way
assert not called
another_button._dom_element.click()
await call_flag.wait()
assert called
async def test_on_event_with_default_handler(self):
called = False
call_flag = asyncio.Event()
def handler(event):
nonlocal called
called = True
call_flag.set()
b = web.button("Click me", on_click=handler)
# Now let's simulate a click on the button (using the low level JS API)
# so we don't risk dom getting in the way
assert not called
b._dom_element.click()
await call_flag.wait()
assert called
def test_on_event_must_be_actual_event(self):
"""
Any on_FOO event must relate to an actual FOO event on the element.
"""
b = web.button("Click me")
# Non-existent event causes a ValueError
with upytest.raises(ValueError):
b.on_chicken
# Buttons have an underlying "click" event so this will work.
assert b.on_click
def test_inner_html_attribute(self): def test_inner_html_attribute(self):
# GIVEN an existing element on the page with a known empty text content # GIVEN an existing element on the page with a known empty text content
div = web.page.find("#element_attribute_tests")[0] div = web.page.find("#element_attribute_tests")[0]
@@ -227,11 +278,15 @@ class TestCollection:
assert el.style["background-color"] != "red" assert el.style["background-color"] != "red"
assert elements[i].style["background-color"] != "red" assert elements[i].style["background-color"] != "red"
@upytest.skip(
"Flakey in Pyodide on Worker",
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
)
async def test_when_decorator(self): async def test_when_decorator(self):
called = False called = False
call_flag = asyncio.Event() call_flag = asyncio.Event()
buttons_collection = web.page.find("button") buttons_collection = web.page["button"]
@when("click", buttons_collection) @when("click", buttons_collection)
def on_click(event): def on_click(event):
@@ -249,6 +304,28 @@ class TestCollection:
called = False called = False
call_flag.clear() call_flag.clear()
async def test_when_decorator_on_event(self):
call_counter = 0
call_flag = asyncio.Event()
buttons_collection = web.page.find("button")
number_of_clicks = len(buttons_collection)
@when(buttons_collection.on_click)
def on_click(event):
nonlocal call_counter
call_counter += 1
if call_counter == number_of_clicks:
call_flag.set()
# Now let's simulate a click on the button (using the low level JS API)
# so we don't risk dom getting in the way
assert call_counter == 0
for button in buttons_collection:
button._dom_element.click()
await call_flag.wait()
assert call_counter == number_of_clicks
class TestCreation: class TestCreation:
@@ -759,14 +836,13 @@ class TestElements:
self._create_el_and_basic_asserts("iframe", properties=properties) self._create_el_and_basic_asserts("iframe", properties=properties)
@upytest.skip( @upytest.skip(
"Flakey on Pyodide in worker.", "Flakey in worker.",
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython, skip_when=RUNNING_IN_WORKER,
) )
async def test_img(self): async def test_img(self):
""" """
This test contains a bespoke version of the _create_el_and_basic_asserts This test, thanks to downloading an image from the internet, is flakey
function so we can await asyncio.sleep if in a worker, so the DOM state when run in a worker. It's skipped when running in a worker.
is in sync with the worker before property based asserts can happen.
""" """
properties = { properties = {
"src": "https://picsum.photos/600/400", "src": "https://picsum.photos/600/400",
@@ -774,39 +850,7 @@ class TestElements:
"width": 250, "width": 250,
"height": 200, "height": 200,
} }
self._create_el_and_basic_asserts("img", properties=properties)
def parse_value(v):
if isinstance(v, bool):
return str(v)
return f"{v}"
args = []
kwargs = {}
if properties:
kwargs = {k: parse_value(v) for k, v in properties.items()}
# Let's make sure the target div to contain the element is empty.
container = web.page["#test-element-container"][0]
container.innerHTML = ""
assert container.innerHTML == "", container.innerHTML
# Let's create the element
try:
klass = getattr(web, "img")
el = klass(*args, **kwargs)
container.append(el)
except Exception as e:
assert False, f"Failed to create element img: {e}"
if RUNNING_IN_WORKER:
# Needed to sync the DOM with the worker.
await asyncio.sleep(0.5)
# Check the img element was created correctly and all its properties
# were set correctly.
for k, v in properties.items():
assert v == getattr(el, k), f"{k} should be {v} but is {getattr(el, k)}"
def test_input(self): def test_input(self):
# TODO: we need multiple input tests # TODO: we need multiple input tests

View File

@@ -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)

View File

@@ -1,6 +1,6 @@
declare var b: any; declare var v: any;
declare var I: boolean; declare var k: boolean;
declare namespace r { declare namespace i {
export let __esModule: boolean; export let __esModule: boolean;
export { Readline }; export { Readline };
} }
@@ -57,7 +57,7 @@ declare class Readline {
highlighter: any; highlighter: any;
history: any; history: any;
promptSize: any; promptSize: any;
layout: p; layout: c;
buffer(): string; buffer(): string;
shouldHighlight(): boolean; shouldHighlight(): boolean;
clearScreen(): void; clearScreen(): void;
@@ -124,15 +124,15 @@ declare class Readline {
readPaste(t: any): void; readPaste(t: any): void;
readKey(t: any): void; readKey(t: any): void;
} }
declare class p { declare class c {
constructor(t: any); constructor(t: any);
promptSize: any; promptSize: any;
cursor: c; cursor: u;
end: c; end: u;
} }
declare class c { declare class u {
constructor(t: any, e: any); constructor(t: any, e: any);
row: any; row: any;
col: any; col: any;
} }
export { b as Readline, I as __esModule, r as default }; export { v as Readline, k as __esModule, i as default };

View File

@@ -1,4 +1,4 @@
declare var i: any; declare var D: any;
declare var s: any; declare var R: any;
declare var t: {}; declare var L: {};
export { i as Terminal, s as __esModule, t as default }; export { D as Terminal, R as __esModule, L as default };

12
core/types/core.d.ts vendored
View File

@@ -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; export function offline_interpreter(config: any): string;
import codemirror from "./plugins/codemirror.js";
import { stdlib } from "./stdlib.js"; import { stdlib } from "./stdlib.js";
import { optional } from "./stdlib.js"; import { optional } from "./stdlib.js";
import { inputFailure } from "./hooks.js"; import { inputFailure } from "./hooks.js";
@@ -56,4 +64,4 @@ declare const exportedHooks: {
}; };
declare const exportedConfig: {}; declare const exportedConfig: {};
declare const exportedWhenDefined: any; declare const exportedWhenDefined: any;
export { stdlib, optional, inputFailure, TYPES, relative_url, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined }; export { codemirror, stdlib, optional, inputFailure, TYPES, relative_url, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };

View File

@@ -1,4 +1,5 @@
declare const _default: { declare const _default: {
codemirror: () => Promise<typeof import("./plugins/codemirror.js")>;
"deprecations-manager": () => Promise<typeof import("./plugins/deprecations-manager.js")>; "deprecations-manager": () => Promise<typeof import("./plugins/deprecations-manager.js")>;
donkey: () => Promise<typeof import("./plugins/donkey.js")>; donkey: () => Promise<typeof import("./plugins/donkey.js")>;
error: () => Promise<typeof import("./plugins/error.js")>; error: () => Promise<typeof import("./plugins/error.js")>;

9
core/types/plugins/codemirror.d.ts vendored Normal file
View 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;

View File

@@ -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; export default _default;

View File

@@ -1,3 +1,4 @@
export function notOnDOM(): void;
/** /**
* Add a banner to the top of the page, notifying the user of an error * Add a banner to the top of the page, notifying the user of an error
* @param {string} message * @param {string} message

View File

@@ -2,7 +2,7 @@ declare namespace _default {
let pyscript: { let pyscript: {
"__init__.py": string; "__init__.py": string;
"display.py": string; "display.py": string;
"event_handling.py": string; "events.py": string;
"fetch.py": string; "fetch.py": string;
"ffi.py": string; "ffi.py": string;
"flatted.py": string; "flatted.py": string;

View File

@@ -39,6 +39,15 @@
#header p { #header p {
font-size: 1.4em; font-size: 1.4em;
} }
#header > svg {
max-width: 480px;
}
@media only screen and (max-width: 400px) {
#header > svg {
width: 256px;
height: 87px;
}
}
</style> </style>
<script> <script>
function copyToClipboard() { function copyToClipboard() {
@@ -53,12 +62,7 @@
<body> <body>
<main> <main>
<div id="header"> <div id="header">
<svg <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 173">
xmlns="http://www.w3.org/2000/svg"
width="512"
height="173"
viewBox="0 0 512 173"
>
<path <path
fill="#fda703" fill="#fda703"
d="M170.14 16.147c12.97-12.966 34.252-21.918 51.437-11.75c3.776 2.234 7.3 5.063 10.242 8.321c9.075 10.047 11.853 24.04 11.915 37.193c.125 26.222-9.355 56.093-34.555 68.782c-9.635 4.851-20.398 6.592-31.126 6.122c-6.042-.265-12.118-1.49-18.162-1.568l-.567-.004v46.425h-20.047V3.222h20.047v26.642h.264c2.19-5.078 6.687-9.854 10.551-13.717m1.43 20.312c-1.706 1.975-3.301 4.022-4.862 6.111l-1.033 1.386c-2.247 3-4.878 6.491-6.095 9.912c-.91 2.559-.256 6.27-.256 8.969v42.469l.845.173c13.804 2.808 27.723 4.557 40.305-3.454c17.28-11.002 22.71-32.58 21.39-51.85l-.04-.559c-.678-9.13-2.678-18.348-9.484-25.017c-13.438-13.167-31.313.912-40.77 11.86M261.67 3.222c.852 2.961 2.596 5.653 3.875 8.441c3.134 6.827 6.502 13.549 9.773 20.311c9.214 19.044 18.41 38.111 27.444 57.24c2.807 5.944 5.718 11.838 8.551 17.768l1.21 2.544c1.239 2.626 3.65 5.807 4.053 8.705c.196 1.397-.933 3.115-1.467 4.396l-.036.088a79.5 79.5 0 0 1-5.605 11.343l-.86 1.439c-1.961 3.273-4.013 6.592-6.603 9.376l-.392.42c-2.033 2.167-4.196 4.296-6.706 5.896c-11.047 7.047-23.326 5.408-35.347 2.652v17.674l.842.107c13.763 1.742 27.7 2.564 40.572-3.871c13.107-6.554 21-19.512 27.502-32.11c11.882-23.024 20.787-47.708 30.778-71.61l.39-.93C365.137 50.016 370.51 36.88 376 23.797c1.806-4.304 3.56-8.631 5.389-12.925c1.036-2.433 2.49-5.036 2.94-7.65h-14.243c-1.433 0-4.197-.528-5.437.271c-1.488.96-2.137 4.48-2.815 6.06c-2.363 5.496-4.505 11.088-6.787 16.618c-6.304 15.28-12.598 30.568-18.778 45.898c-2.72 6.742-6.71 13.77-8.389 20.839h-.527l-3.89-8.177l-6.857-13.98l-21.368-43.789l-8.31-16.882l-3.532-6.587l-5.898-.27zM0 64.683c2.398 1.987 5.918 3.156 8.705 4.55l71.748 35.874c7.172 3.586 14.3 7.25 21.46 10.856l3.072 1.542c3.547 1.773 7.697 4.777 11.606 5.474v-12.925l-.27-5.173l-6.06-3.334l-15.3-7.65l-40.358-20.31l-17.674-8.904l17.674-8.77l40.095-19.917l15.563-7.78l6.06-3.268l.27-5.108V6.651c-4.078.727-8.432 3.888-12.134 5.738L26.378 51.43C17.63 55.804 8.446 59.754 0 64.683M395.672 6.651v12.926c0 1.382-.496 3.972.272 5.172c.958 1.5 4.234 2.423 5.795 3.203l15.036 7.517c13.292 6.646 26.536 13.4 39.83 20.047c5.92 2.96 12.1 7.333 18.466 9.167v.528c-6.835 1.969-13.68 6.511-20.048 9.695l-38.512 19.256c-4.635 2.317-9.224 4.866-13.98 6.923l-.184.078c-1.805.75-5.401 1.885-6.403 3.454c-.768 1.2-.272 3.79-.272 5.173v13.189l14.508-7.057l77.025-38.512L512 64.947c-2.654-2.68-7.705-4.182-11.079-5.868L404.905 11.07c-2.965-1.483-6.044-3.5-9.233-4.419" d="M170.14 16.147c12.97-12.966 34.252-21.918 51.437-11.75c3.776 2.234 7.3 5.063 10.242 8.321c9.075 10.047 11.853 24.04 11.915 37.193c.125 26.222-9.355 56.093-34.555 68.782c-9.635 4.851-20.398 6.592-31.126 6.122c-6.042-.265-12.118-1.49-18.162-1.568l-.567-.004v46.425h-20.047V3.222h20.047v26.642h.264c2.19-5.078 6.687-9.854 10.551-13.717m1.43 20.312c-1.706 1.975-3.301 4.022-4.862 6.111l-1.033 1.386c-2.247 3-4.878 6.491-6.095 9.912c-.91 2.559-.256 6.27-.256 8.969v42.469l.845.173c13.804 2.808 27.723 4.557 40.305-3.454c17.28-11.002 22.71-32.58 21.39-51.85l-.04-.559c-.678-9.13-2.678-18.348-9.484-25.017c-13.438-13.167-31.313.912-40.77 11.86M261.67 3.222c.852 2.961 2.596 5.653 3.875 8.441c3.134 6.827 6.502 13.549 9.773 20.311c9.214 19.044 18.41 38.111 27.444 57.24c2.807 5.944 5.718 11.838 8.551 17.768l1.21 2.544c1.239 2.626 3.65 5.807 4.053 8.705c.196 1.397-.933 3.115-1.467 4.396l-.036.088a79.5 79.5 0 0 1-5.605 11.343l-.86 1.439c-1.961 3.273-4.013 6.592-6.603 9.376l-.392.42c-2.033 2.167-4.196 4.296-6.706 5.896c-11.047 7.047-23.326 5.408-35.347 2.652v17.674l.842.107c13.763 1.742 27.7 2.564 40.572-3.871c13.107-6.554 21-19.512 27.502-32.11c11.882-23.024 20.787-47.708 30.778-71.61l.39-.93C365.137 50.016 370.51 36.88 376 23.797c1.806-4.304 3.56-8.631 5.389-12.925c1.036-2.433 2.49-5.036 2.94-7.65h-14.243c-1.433 0-4.197-.528-5.437.271c-1.488.96-2.137 4.48-2.815 6.06c-2.363 5.496-4.505 11.088-6.787 16.618c-6.304 15.28-12.598 30.568-18.778 45.898c-2.72 6.742-6.71 13.77-8.389 20.839h-.527l-3.89-8.177l-6.857-13.98l-21.368-43.789l-8.31-16.882l-3.532-6.587l-5.898-.27zM0 64.683c2.398 1.987 5.918 3.156 8.705 4.55l71.748 35.874c7.172 3.586 14.3 7.25 21.46 10.856l3.072 1.542c3.547 1.773 7.697 4.777 11.606 5.474v-12.925l-.27-5.173l-6.06-3.334l-15.3-7.65l-40.358-20.31l-17.674-8.904l17.674-8.77l40.095-19.917l15.563-7.78l6.06-3.268l.27-5.108V6.651c-4.078.727-8.432 3.888-12.134 5.738L26.378 51.43C17.63 55.804 8.446 59.754 0 64.683M395.672 6.651v12.926c0 1.382-.496 3.972.272 5.172c.958 1.5 4.234 2.423 5.795 3.203l15.036 7.517c13.292 6.646 26.536 13.4 39.83 20.047c5.92 2.96 12.1 7.333 18.466 9.167v.528c-6.835 1.969-13.68 6.511-20.048 9.695l-38.512 19.256c-4.635 2.317-9.224 4.866-13.98 6.923l-.184.078c-1.805.75-5.401 1.885-6.403 3.454c-.768 1.2-.272 3.79-.272 5.173v13.189l14.508-7.057l77.025-38.512L512 64.947c-2.654-2.68-7.705-4.182-11.079-5.868L404.905 11.07c-2.965-1.483-6.044-3.5-9.233-4.419"