Compare commits

..

1 Commits

Author SHA1 Message Date
webreflection
c0fcfa7a07 WIP - PyGame-CE - main thread only so far 2024-12-10 15:53:49 +01:00
71 changed files with 594 additions and 2293 deletions

View File

@@ -25,29 +25,22 @@ repos:
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 25.1.0
rev: 24.10.0
hooks:
- id: black
exclude: core/tests
args: ["-l", "88", "--skip-string-normalization"]
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
rev: v2.3.0
hooks:
- id: codespell # See 'pyproject.toml' for args
exclude: fs\.py|\.js\.map$
exclude: \.js\.map$
additional_dependencies:
- tomli
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
hooks:
- id: ruff
exclude: core/tests
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
rev: "v3.0.0-alpha.6"
hooks:
- id: prettier
exclude: core/tests|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
exclude: core/test|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
args: [--tab-width, "4"]

View File

@@ -41,8 +41,8 @@ check-python:
# Check the environment, install the dependencies.
setup: check-node check-npm check-python
cd core && npm ci && cd ..
ifeq (,$(VIRTUAL_ENV)$(CONDA_PREFIX))
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv or conda env is not activated.\033[0m"
ifeq ($(VIRTUAL_ENV),)
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
false
else
python -m pip install -r requirements.txt

View File

@@ -83,12 +83,3 @@ documentation for more information on how to setup your development environment.
The [PyScript organization governance](https://github.com/pyscript/governance)
is documented in a separate repository.
## Supporters
PyScript is an independent open source project.
However, PyScript was born at [Anaconda Inc](https://anaconda.com/) and its
core contributors are currently employed by Anaconda to work on PyScript. We
would like to acknowledge and celebrate Anaconda's continued support of this
project. Thank you [Anaconda Inc](https://anaconda.com/)!

724
core/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@pyscript/core",
"version": "0.6.39",
"version": "0.6.22",
"type": "module",
"description": "PyScript",
"module": "./index.js",
@@ -25,10 +25,6 @@
"types": "./types/core.d.ts",
"import": "./src/core.js"
},
"./js": {
"types": "./types/core.d.ts",
"import": "./dist/core.js"
},
"./css": {
"import": "./dist/core.css"
},
@@ -47,7 +43,7 @@
"build:3rd-party": "node rollup/3rd-party.cjs",
"build:tests-index": "node rollup/build_test_index.cjs",
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; (playwright test tests/js_tests.spec.js && playwright test tests/py_tests.main.spec.js && playwright test tests/py_tests.worker.spec.js) || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test tests/js_tests.spec.js tests/py_tests.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
"test:ws": "bun tests/javascript/ws/index.js & playwright test tests/javascript/ws/index.spec.js",
"dev": "node dev.cjs",
"release": "npm run build && npm run zip",
@@ -66,38 +62,38 @@
"@webreflection/idb-map": "^0.3.2",
"add-promise-listener": "^0.1.3",
"basic-devtools": "^0.1.6",
"polyscript": "^0.16.22",
"sabayon": "^0.6.6",
"polyscript": "^0.16.10",
"sabayon": "^0.6.1",
"sticky-module": "^0.1.1",
"to-json-callback": "^0.1.1",
"type-checked-collections": "^0.1.7"
},
"devDependencies": {
"@codemirror/commands": "^6.8.0",
"@codemirror/lang-python": "^6.1.7",
"@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.4",
"@playwright/test": "^1.51.1",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@codemirror/commands": "^6.7.1",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/language": "^6.10.6",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.35.0",
"@playwright/test": "1.45.3",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@webreflection/toml-j0.4": "^1.1.3",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"bun": "^1.2.5",
"chokidar": "^4.0.3",
"bun": "^1.1.38",
"chokidar": "^4.0.1",
"codedent": "^0.1.2",
"codemirror": "^6.0.1",
"eslint": "^9.22.0",
"flatted": "^3.3.3",
"rollup": "^4.36.0",
"eslint": "^9.16.0",
"flatted": "^3.3.2",
"rollup": "^4.28.1",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0",
"static-handler": "^0.5.3",
"string-width": "^7.2.0",
"typescript": "^5.8.2",
"typescript": "^5.7.2",
"xterm-readline": "^1.1.2"
},
"repository": {

View File

@@ -1,5 +1,5 @@
/**
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.37.0.
* Bundled by jsDelivr using Rollup v2.79.1 and Terser v5.19.2.
* Original file: /npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files

View File

@@ -1,5 +1,5 @@
/**
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.37.0.
* Bundled by jsDelivr using Rollup v2.79.1 and Terser v5.19.2.
* Original file: /npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files

View File

@@ -25,7 +25,7 @@ const badURL = (url, expected = "") => {
* @param {string?} type the optional type to enforce
* @returns {{json: boolean, toml: boolean, text: string}}
*/
export const configDetails = async (config, type) => {
const configDetails = async (config, type) => {
let text = config?.trim();
// we only support an object as root config
let url = "",

View File

@@ -28,34 +28,53 @@ mpy-config {
.py-editor-run-button,
.mpy-editor-run-button {
position: absolute;
display: flex;
right: 0.5rem;
bottom: 0.5rem;
opacity: 0;
transition: opacity 0.25s;
z-index: 1;
padding: 0;
}
.py-editor-box:hover .py-editor-run-button,
.mpy-editor-box:hover .mpy-editor-run-button,
.py-editor-run-button:focus,
.py-editor-run-button.running,
.py-editor-run-button:disabled,
.mpy-editor-run-button:focus,
.mpy-editor-run-button.running {
.mpy-editor-run-button:disabled {
opacity: 1;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.py-editor-run-button:disabled > *,
.mpy-editor-run-button:disabled > * {
display: none; /* hide all the child elements of the run button when it is disabled */
}
.py-editor-run-button:disabled,
.mpy-editor-run-button:disabled {
border-width: 0;
}
.py-editor-run-button:disabled::before,
.mpy-editor-run-button:disabled::before {
content: "";
box-sizing: border-box;
position: absolute;
top: 100%;
left: 100%;
width: 20px;
height: 20px;
margin-top: -23px; /* hardcoded value to center the spinner on the run button */
margin-left: -26px; /* hardcoded value to center the spinner on the run button */
border-radius: 50%;
border: 2px solid #aaa;
border-top-color: #000;
background-color: #fff;
animation: spinner 0.6s linear infinite;
}
py-terminal span,
mpy-terminal span {
letter-spacing: 0 !important;
}
dialog.pyscript-fs {
border-radius: 8px;
border-width: 1px;
}
dialog.pyscript-fs > div {
display: flex;
justify-content: space-between;
}

View File

@@ -33,7 +33,6 @@ import {
createFunction,
inputFailure,
} from "./hooks.js";
import * as fs from "./fs.js";
import codemirror from "./plugins/codemirror.js";
export { codemirror };
@@ -168,8 +167,6 @@ for (const [TYPE, interpreter] of TYPES) {
// enrich the Python env with some JS utility for main
interpreter.registerJsModule("_pyscript", {
PyWorker,
fs,
interpreter,
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
get target() {
return isScript(currentElement)
@@ -226,6 +223,13 @@ for (const [TYPE, interpreter] of TYPES) {
else element.after(show);
}
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
// document.currentScript.target if needed

View File

@@ -1,81 +0,0 @@
import IDBMap from "@webreflection/idb-map";
import { assign } from "polyscript/exports";
import { $$ } from "basic-devtools";
const stop = (event) => {
event.preventDefault();
event.stopImmediatePropagation();
};
// ⚠️ these two constants MUST be passed as `fs`
// within the worker onBeforeRunAsync hook!
export const NAMESPACE = "@pyscript.fs";
export const ERROR = "storage permissions not granted";
export const idb = new IDBMap(NAMESPACE);
/**
* Ask a user action via dialog and returns the directory handler once granted.
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
* @returns {Promise<FileSystemDirectoryHandle>}
*/
export const getFileSystemDirectoryHandle = async (options) => {
if (!("showDirectoryPicker" in globalThis)) {
return Promise.reject(
new Error("showDirectoryPicker is not supported"),
);
}
const { promise, resolve, reject } = Promise.withResolvers();
const how = { id: "pyscript", mode: "readwrite", ...options };
if (options.hint) how.startIn = options.hint;
const transient = async () => {
try {
/* eslint-disable */
const handler = await showDirectoryPicker(how);
/* eslint-enable */
if ((await handler.requestPermission(how)) === "granted") {
resolve(handler);
return true;
}
} catch ({ message }) {
console.warn(message);
}
return false;
};
// in case the user decided to attach the event itself
// as opposite of relying our dialog walkthrough
if (navigator.userActivation?.isActive) {
if (!(await transient())) reject(new Error(ERROR));
} else {
const dialog = assign(document.createElement("dialog"), {
className: "pyscript-fs",
innerHTML: [
"<strong> Persistent FileSystem</strong><hr>",
"<p><small>PyScript would like to access a local folder.</small></p>",
"<div><button title='ok'>✅ Authorize</button>",
"<button title='cancel'>❌</button></div>",
].join(""),
});
const [ok, cancel] = $$("button", dialog);
ok.addEventListener("click", async (event) => {
stop(event);
if (await transient()) dialog.close();
});
cancel.addEventListener("click", async (event) => {
stop(event);
reject(new Error(ERROR));
dialog.close();
});
document.body.appendChild(dialog).showModal();
}
return promise;
};

View File

@@ -84,23 +84,23 @@ export const hooks = {
},
worker: {
/** @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>} */
onBeforeRun: new SetFunction(),
/** @type {Set<function>} */
onBeforeRunAsync: new SetFunction([
({ interpreter }) => {
interpreter.registerJsModule("_pyscript", {
// cannot be imported from fs.js
// because this code is stringified
fs: {
ERROR: "storage permissions not granted",
NAMESPACE: "@pyscript.fs",
},
interpreter,
});
},
]),
onBeforeRunAsync: new SetFunction(),
/** @type {Set<function>} */
onAfterRun: new SetFunction(),
/** @type {Set<function>} */

View File

@@ -25,11 +25,6 @@ export default {
/* webpackIgnore: true */
"./plugins/py-editor.js"
),
["py-game"]: () =>
import(
/* webpackIgnore: true */
"./plugins/py-game.js"
),
["py-terminal"]: () =>
import(
/* webpackIgnore: true */

View File

@@ -5,13 +5,7 @@ const { stringify } = JSON;
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
const donkey = ({
type = "py",
persistent,
terminal,
config,
serviceWorker,
}) => {
const donkey = ({ type = "py", persistent, terminal, config }) => {
const globals = terminal ? '{"__terminal__":__terminal__}' : "{}";
const args = persistent ? ["globals()", "__locals__"] : [globals, "{}"];
@@ -52,7 +46,6 @@ const donkey = ({
typeof config === "string" ? config : stringify(config),
);
}
if (serviceWorker) script.setAttribute("service-worker", serviceWorker);
return addPromiseListener(
document.body.appendChild(script),

View File

@@ -4,15 +4,13 @@ import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
import { notify } from "./error.js";
import codemirror from "./codemirror.js";
const RUN_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,12a1,1,0,0,1-.55.89l-10,5A1,1,0,0,1,8,18a1,1,0,0,1-.53-.15A1,1,0,0,1,7,17V7a1,1,0,0,1,1.45-.89l10,5A1,1,0,0,1,19,12Z" fill="#464646"/></svg>`;
const STOP_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 7h10v10H7z" style="fill:#464646;stroke:#464646;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"/></svg>`;
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>`;
let id = 0;
const getID = (type) => `${type}-editor-${id++}`;
const envs = new Map();
const configs = new Map();
const editors = new WeakMap();
const hooks = {
worker: {
@@ -32,18 +30,12 @@ const validate = (config, result) => {
return result;
};
const getRelatedScript = (target, type) => {
const editor = target.closest(`.${type}-editor-box`);
return editor?.parentNode?.previousElementSibling;
};
async function execute({ currentTarget }) {
const { env, pySrc, outDiv } = this;
const hasRunButton = !!currentTarget;
if (hasRunButton) {
currentTarget.classList.add("running");
currentTarget.innerHTML = STOP_BUTTON;
currentTarget.disabled = true;
outDiv.innerHTML = "";
}
@@ -90,7 +82,8 @@ async function execute({ currentTarget }) {
// creation and destruction of editors on the fly
if (hasRunButton) {
for (const type of TYPES.keys()) {
const script = getRelatedScript(currentTarget, type);
const editor = currentTarget.closest(`.${type}-editor-box`);
const script = editor?.parentNode?.previousElementSibling;
if (script) {
defineProperties(script, { xworker: { value: xworker } });
break;
@@ -123,10 +116,7 @@ async function execute({ currentTarget }) {
};
const enable = () => {
if (hasRunButton) {
currentTarget.classList.remove("running");
currentTarget.innerHTML = RUN_BUTTON;
}
if (hasRunButton) currentTarget.disabled = false;
};
const { sync } = xworker;
sync.write = (str) => {
@@ -154,24 +144,6 @@ const makeRunButton = (handler, type) => {
runButton.innerHTML = RUN_BUTTON;
runButton.setAttribute("aria-label", "Python Script Run Button");
runButton.addEventListener("click", async (event) => {
if (
runButton.classList.contains("running") &&
confirm("Stop evaluating this code?")
) {
const script = getRelatedScript(runButton, type);
if (script) {
const editor = editors.get(script);
const content = editor.state.doc.toString();
const clone = script.cloneNode(true);
clone.type = `${type}-editor`;
clone.textContent = content;
script.xworker.terminate();
script.nextElementSibling.remove();
script.replaceWith(clone);
editors.delete(script);
}
return;
}
runButton.blur();
await handler.handleEvent(event);
});
@@ -415,7 +387,6 @@ const init = async (script, type, interpreter) => {
doc,
});
editors.set(script, editor);
editor.focus();
notifyEditor();
};

View File

@@ -1,112 +0,0 @@
import {
dedent,
define,
createProgress,
loadProgress,
} from "polyscript/exports";
import { stdlib } from "../core.js";
import { configDetails } from "../config.js";
import { getText } from "../fetch.js";
const progress = createProgress("py-game");
const inputPatch = `
import builtins
def input(prompt=""):
import js
return js.prompt(prompt)
builtins.input = input
del builtins
del input
`;
let toBeWarned = true;
const hooks = {
main: {
onReady: async (wrap, script) => {
if (toBeWarned) {
toBeWarned = false;
console.warn("⚠️ EXPERIMENTAL `py-game` FEATURE");
}
let config = {};
if (script.hasAttribute("config")) {
const value = script.getAttribute("config");
const { json, toml, text, url } = await configDetails(value);
if (json) config = JSON.parse(text);
else if (toml) {
const { parse } = await import(
/* webpackIgnore: true */ "../3rd-party/toml.js"
);
config = parse(text);
}
if (config.packages) {
await wrap.interpreter.loadPackage("micropip");
const micropip = wrap.interpreter.pyimport("micropip");
await micropip.install(config.packages, {
keep_going: true,
});
micropip.destroy();
}
await loadProgress(
"py-game",
progress,
wrap.interpreter,
config,
url ? new URL(url, location.href).href : location.href,
);
}
wrap.interpreter.registerJsModule("_pyscript", {
PyWorker() {
throw new Error(
"Unable to use PyWorker in py-game scripts",
);
},
js_import: (...urls) =>
Promise.all(urls.map((url) => import(url))),
get target() {
return script.id;
},
});
await wrap.interpreter.runPythonAsync(stdlib);
wrap.interpreter.runPython(inputPatch);
let code = dedent(script.textContent);
if (script.src) code = await fetch(script.src).then(getText);
const target = script.getAttribute("target") || "canvas";
const canvas = document.getElementById(target);
wrap.interpreter.canvas.setCanvas2D(canvas);
// allow 3rd party to hook themselves right before
// the code gets executed
const event = new CustomEvent("py-game", {
bubbles: true,
cancelable: true,
detail: {
canvas,
code,
config,
wrap,
},
});
script.dispatchEvent(event);
// run only if the default was not prevented
if (!event.defaultPrevented)
await wrap.interpreter.runPythonAsync(code);
},
},
};
define("py-game", {
config: { packages: ["pygame-ce"] },
configURL: new URL("./config.txt", location.href).href,
interpreter: "pyodide",
env: "py-game",
hooks,
});

File diff suppressed because one or more lines are too long

View File

@@ -73,14 +73,14 @@ def _eval_formatter(obj, print_method):
"""
if print_method == "__repr__":
return repr(obj)
if hasattr(obj, print_method):
elif hasattr(obj, print_method):
if print_method == "savefig":
buf = io.BytesIO()
obj.savefig(buf, format="png")
buf.seek(0)
return base64.b64encode(buf.read()).decode("utf-8")
return getattr(obj, print_method)()
if print_method == "_repr_mimebundle_":
elif print_method == "_repr_mimebundle_":
return {}, {}
return None
@@ -107,7 +107,7 @@ def _format_mime(obj):
if output is None:
continue
if mime_type not in _MIME_RENDERERS:
elif mime_type not in _MIME_RENDERERS:
not_available.append(mime_type)
continue
break
@@ -149,11 +149,9 @@ def display(*values, target=None, append=True):
if target is None:
target = current_target()
elif not isinstance(target, str):
msg = f"target must be str or None, not {target.__class__.__name__}"
raise TypeError(msg)
raise TypeError(f"target must be str or None, not {target.__class__.__name__}")
elif target == "":
msg = "Cannot have an empty target"
raise ValueError(msg)
raise ValueError("Cannot have an empty target")
elif target.startswith("#"):
# note: here target is str and not None!
# align with @when behavior
@@ -163,8 +161,9 @@ def display(*values, target=None, append=True):
# If target cannot be found on the page, a ValueError is raised
if element is None:
msg = f"Invalid selector with id={target}. Cannot be found in the page."
raise ValueError(msg)
raise ValueError(
f"Invalid selector with id={target}. Cannot be found in the page."
)
# if element is a <script type="py">, it has a 'target' attribute which
# points to the visual element holding the displayed values. In that case,

View File

@@ -1,5 +1,6 @@
import asyncio
import inspect
import sys
from functools import wraps
from pyscript.magic_js import document
@@ -10,87 +11,37 @@ from pyscript import config
class Event:
"""
Events represent something that may happen at some point in time (usually
the future). They're used to coordinate code when the timing of an event is
not known in advance (e.g. a button click or a network response).
An event is triggered with an arbitrary result. If no result is given, then
None is assumed as the result.
Add listener functions to the event, to be called with the result when the
event is triggered. The listener functions can be callable or awaitable. If
the listener is added several times, it will be called only once.
If the event was triggered before a listener is added, the listener will be
called as soon as it is added, with the result of the event.
If the event is never triggered, then its listeners will never be called.
It's also possible to remove listeners from the event.
If the result of the event is not available, a ValueError will be raised
when trying to access the result property. A RuntimeError will be raised if
the event is triggered more than once.
Represents something that may happen at some point in the future.
"""
def __init__(self):
# To contain the listeners to be called when the event is triggered.
self._listeners = []
# The result associated with the event.
self._result = None
# A flag to indicate if the event has been triggered.
self._triggered = False
@property
def triggered(self):
def trigger(self, result):
"""
A boolean flag to indicate if the event has been triggered.
Trigger the event with a result to pass into the handlers.
"""
return self._triggered
@property
def result(self):
"""
The result of the event.
"""
if self.triggered:
return self._result
msg = "Event has not been triggered yet. No result available."
raise ValueError(msg)
def trigger(self, result=None):
"""
Trigger the event with an arbitrary result to pass into the listeners.
An event may only be triggered once (otherwise a RuntimeError is
raised).
"""
if self.triggered:
msg = "Event has already been triggered."
raise RuntimeError(msg)
self._triggered = True
self._result = result
for listener in self._listeners:
self._call_listener(listener)
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 that listens for the result, when this event
is triggered.
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)
if self.triggered:
# If the event was already triggered, call the listener
# immediately with the result.
self._call_listener(listener)
else:
msg = "Listener must be callable or awaitable."
raise ValueError(msg)
raise ValueError("Listener must be callable or awaitable.")
def remove_listener(self, *args):
"""
Clear the specified listener functions in *args. If no listeners are
provided, clear all the listeners.
Clear the specified handler functions in *args. If no handlers
provided, clear all handlers.
"""
if args:
for listener in args:
@@ -98,15 +49,6 @@ class Event:
else:
self._listeners = []
def _call_listener(self, listener):
"""
Call the referenced listener with the event's result.
"""
if is_awaitable(listener):
asyncio.create_task(listener(self._result))
else:
listener(self._result)
def when(target, *args, **kwargs):
"""
@@ -134,8 +76,7 @@ def when(target, *args, **kwargs):
# Extract the selector from the arguments or keyword arguments.
selector = args[0] if args else kwargs.pop("selector")
if not selector:
msg = "No selector provided."
raise ValueError(msg)
raise ValueError("No selector provided.")
# Grab the DOM elements to which the target event will be attached.
from pyscript.web import Element, ElementCollection

View File

@@ -31,7 +31,7 @@ def _object_keys(value):
def _is_array(value):
return isinstance(value, (list, tuple))
return isinstance(value, list) or isinstance(value, tuple)
def _is_object(value):
@@ -60,10 +60,10 @@ def _loop(keys, input, known, output):
def _ref(key, value, input, known, output):
if _is_array(value) and value not in known:
if _is_array(value) and not value in known:
known.append(value)
value = _loop(_array_keys(value), input, known, value)
elif _is_object(value) and value not in known:
elif _is_object(value) and not value in known:
known.append(value)
value = _loop(_object_keys(value), input, known, value)

View File

@@ -1,60 +0,0 @@
mounted = {}
async def mount(path, mode="readwrite", root="", id="pyscript"):
import js
from _pyscript import fs, interpreter
from pyscript.ffi import to_js
from pyscript.magic_js import (
RUNNING_IN_WORKER,
sync,
)
js.console.warn("experimental pyscript.fs ⚠️")
handler = None
uid = f"{path}@{id}"
options = {"id": id, "mode": mode}
if root != "":
options["startIn"] = root
if RUNNING_IN_WORKER:
fsh = sync.storeFSHandler(uid, to_js(options))
# allow both async and/or SharedArrayBuffer use case
if isinstance(fsh, bool):
success = fsh
else:
success = await fsh
if success:
from polyscript import IDBMap
idb = IDBMap.new(fs.NAMESPACE)
handler = await idb.get(uid)
else:
raise RuntimeError(fs.ERROR)
else:
success = await fs.idb.has(uid)
if success:
handler = await fs.idb.get(uid)
else:
handler = await fs.getFileSystemDirectoryHandle(to_js(options))
await fs.idb.set(uid, handler)
mounted[path] = await interpreter.mountNativeFS(path, handler)
async def sync(path):
await mounted[path].syncfs()
async def unmount(path):
from _pyscript import interpreter
await sync(path)
interpreter._module.FS.unmount(path)

View File

@@ -25,7 +25,6 @@ class JSModule:
# avoid pyodide looking for non existent fields
if not field.startswith("_"):
return getattr(getattr(js_modules, self.name), field)
return None
# generate N modules in the system that will proxy the real value
@@ -46,6 +45,8 @@ if RUNNING_IN_WORKER:
window = polyscript.xworker.window
document = window.document
# weird + not worth it as it does not work anyway
js.screen = window.screen
js.document = document
# this is the same as js_import on main and it lands modules on main
js_import = window.Function(

View File

@@ -31,22 +31,26 @@ class Device:
@classmethod
async def load(cls, audio=False, video=True):
"""
Load the device stream.
"""
options = {}
options["audio"] = audio
"""Load the device stream."""
options = window.Object.new()
options.audio = audio
if isinstance(video, bool):
options["video"] = video
options.video = video
else:
options["video"] = {}
# TODO: Think this can be simplified but need to check it on the pyodide side
# TODO: this is pyodide specific. shouldn't be!
options.video = window.Object.new()
for k in video:
options["video"][k] = video[k]
return await window.navigator.mediaDevices.getUserMedia(to_js(options))
setattr(options.video, k, to_js(video[k]))
stream = await window.navigator.mediaDevices.getUserMedia(options)
return stream
async def get_stream(self):
key = self.kind.replace("input", "").replace("output", "")
options = {key: {"deviceId": {"exact": self.id}}}
return await self.load(**options)

View File

@@ -10,11 +10,10 @@ def _to_idb(value):
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
return _stringify(["generic", value])
if isinstance(value, bytearray):
return _stringify(["bytearray", list(value)])
return _stringify(["bytearray", [v for v in value]])
if isinstance(value, memoryview):
return _stringify(["memoryview", list(value)])
msg = f"Unexpected value: {value}"
raise TypeError(msg)
return _stringify(["memoryview", [v for v in value]])
raise TypeError(f"Unexpected value: {value}")
# convert an IndexedDB compatible entry into a Python value
@@ -57,6 +56,5 @@ class Storage(dict):
async def storage(name="", storage_class=Storage):
if not name:
msg = "The storage name must be defined"
raise ValueError(msg)
raise ValueError("The storage name must be defined")
return storage_class(await _storage(f"@pyscript/{name}"))

View File

@@ -11,7 +11,7 @@ def as_bytearray(buffer):
ui8a = js.Uint8Array.new(buffer)
size = ui8a.length
ba = bytearray(size)
for i in range(size):
for i in range(0, size):
ba[i] = ui8a[i]
return ba

View File

@@ -2,10 +2,7 @@
# `when` is not used in this module. It is imported here save the user an additional
# import (i.e. they can get what they need from `pyscript.web`).
# from __future__ import annotations # CAUTION: This is not supported in MicroPython.
from pyscript import document, when, Event # noqa: F401
from pyscript import document, when, Event # NOQA
from pyscript.ffi import create_proxy
@@ -103,7 +100,7 @@ class Element:
If `key` is an integer or a slice we use it to index/slice the element's
children. Otherwise, we use `key` as a query selector.
"""
if isinstance(key, (int, slice)):
if isinstance(key, int) or isinstance(key, slice):
return self.children[key]
return self.find(key)
@@ -123,7 +120,7 @@ class Element:
# attribute `for` which is a Python keyword, so you can access it on the
# Element instance via `for_`).
if name.endswith("_"):
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
name = name[:-1]
return getattr(self._dom_element, name)
def __setattr__(self, name, value):
@@ -141,7 +138,7 @@ class Element:
# attribute `for` which is a Python keyword, so you can access it on the
# Element instance via `for_`).
if name.endswith("_"):
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
name = name[:-1]
if name.startswith("on_"):
# Ensure on-events are cached in the _on_events dict if the
@@ -155,12 +152,10 @@ class Element:
Get an `Event` instance for the specified event name.
"""
if not name.startswith("on_"):
msg = "Event names must start with 'on_'."
raise ValueError(msg)
raise ValueError("Event names must start with 'on_'.")
event_name = name[3:] # Remove the "on_" prefix.
if not hasattr(self._dom_element, event_name):
msg = f"Element has no '{event_name}' event."
raise ValueError(msg)
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
@@ -208,7 +203,7 @@ class Element:
# We check for list/tuple here and NOT for any iterable as it will match
# a JS Nodelist which is handled explicitly below.
# NodeList.
elif isinstance(item, (list, tuple)):
elif isinstance(item, list) or isinstance(item, tuple):
for child in item:
self.append(child)
@@ -232,11 +227,10 @@ class Element:
except AttributeError:
# Nope! This is not an element or a NodeList.
msg = (
raise TypeError(
f'Element "{item}" is a proxy object, "'
f"but not a valid element or a NodeList."
)
raise TypeError(msg)
def clone(self, clone_id=None):
"""Make a clone of the element (clones the underlying DOM object too)."""
@@ -407,8 +401,9 @@ class Options:
new_option = option(**kwargs)
if before and isinstance(before, Element):
before = before._dom_element
if before:
if isinstance(before, Element):
before = before._dom_element
self._element._dom_element.add(new_option._dom_element, before)
@@ -468,7 +463,7 @@ class ContainerElement(Element):
)
for child in list(args) + (children or []):
if isinstance(child, (Element, ElementCollection)):
if isinstance(child, Element) or isinstance(child, ElementCollection):
self.append(child)
else:
@@ -498,13 +493,14 @@ class ClassesCollection:
)
def __iter__(self):
yield from self._all_class_names()
for class_name in self._all_class_names():
yield class_name
def __len__(self):
return len(self._all_class_names())
def __repr__(self):
return f"ClassesCollection({self._collection!r})"
return f"ClassesCollection({repr(self._collection)})"
def __str__(self):
return " ".join(self._all_class_names())
@@ -557,7 +553,7 @@ class StyleCollection:
element.style[key] = value
def __repr__(self):
return f"StyleCollection({self._collection!r})"
return f"StyleCollection({repr(self._collection)})"
def remove(self, key):
"""Remove a CSS property from the elements in the collection."""
@@ -592,7 +588,7 @@ class ElementCollection:
if isinstance(key, int):
return self._elements[key]
if isinstance(key, slice):
elif isinstance(key, slice):
return ElementCollection(self._elements[key])
return self.find(key)
@@ -1129,8 +1125,7 @@ class video(ContainerElement):
elif isinstance(to, Element):
if to.tag != "canvas":
msg = "Element to snap to must be a canvas."
raise TypeError(msg)
raise TypeError("Element to snap to must be a canvas.")
elif getattr(to, "tagName", "") == "CANVAS":
to = canvas(dom_element=to)
@@ -1139,12 +1134,10 @@ class video(ContainerElement):
elif isinstance(to, str):
nodelist = document.querySelectorAll(to) # NOQA
if nodelist.length == 0:
msg = "No element with selector {to} to snap to."
raise TypeError(msg)
raise TypeError("No element with selector {to} to snap to.")
if nodelist[0].tagName != "CANVAS":
msg = "Element to snap to must be a canvas."
raise TypeError(msg)
raise TypeError("Element to snap to must be a canvas.")
to = canvas(dom_element=nodelist[0])

View File

@@ -24,7 +24,7 @@ class EventMessage:
return value
class WebSocket:
class WebSocket(object):
CONNECTING = 0
OPEN = 1
CLOSING = 2

View File

@@ -25,12 +25,10 @@ async def create_named_worker(src="", name="", config=None, type="py"):
from json import dumps
if not src:
msg = "Named workers require src"
raise ValueError(msg)
raise ValueError("Named workers require src")
if not name:
msg = "Named workers require a name"
raise ValueError(msg)
raise ValueError("Named workers require a name")
s = _js.document.createElement("script")
s.type = type
@@ -39,7 +37,7 @@ async def create_named_worker(src="", name="", config=None, type="py"):
_set(s, "name", name)
if config:
_set(s, "config", (isinstance(config, str) and config) or dumps(config))
_set(s, "config", isinstance(config, str) and config or dumps(config))
_js.document.body.append(s)
return await workers[name]

View File

@@ -1,5 +1,3 @@
import { idb, getFileSystemDirectoryHandle } from "./fs.js";
export default {
// allow pyterminal checks to bootstrap
is_pyterminal: () => false,
@@ -11,21 +9,4 @@ export default {
sleep(seconds) {
return new Promise(($) => setTimeout($, seconds * 1000));
},
/**
* Ask a user action via dialog and returns the directory handler once granted.
* @param {string} uid
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
* @returns {boolean}
*/
async storeFSHandler(uid, options = {}) {
if (await idb.has(uid)) return true;
return getFileSystemDirectoryHandle(options).then(
async (handler) => {
await idb.set(uid, handler);
return true;
},
() => false,
);
},
};

File diff suppressed because one or more lines are too long

View File

@@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Pyodide Media Module Test</title>
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<h1>Pyodide Media Module Test</h1>
<div id="test-results">Running tests...</div>
<script type="py" terminal>
from pyscript import window, document
from pyscript import media
async def run_tests():
# Test basic module structure
assert hasattr(media, "Device"), "media module should have Device class"
assert hasattr(media, "list_devices"), "media module should have list_devices function"
# Test device enumeration
devices = await media.list_devices()
assert isinstance(devices, list), "list_devices should return a list"
# If we have devices, test properties of one
if devices:
device = devices[0]
assert hasattr(device, "id"), "Device should have id property"
assert hasattr(device, "group"), "Device should have group property"
assert hasattr(device, "kind"), "Device should have kind property"
assert hasattr(device, "label"), "Device should have label property"
document.getElementById('test-results').innerText = "Success!"
document.documentElement.classList.add('media-ok')
await run_tests()
</script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
import numpy as np
import matplotlib as mpl
import numpy
import matplotlib
# just do something with the packages
print(len(dir(np)))
print(len(dir(mpl)))
print(len(dir(numpy)))
print(len(dir(matplotlib)))

View File

@@ -4,4 +4,4 @@ def runtime_version():
return sys.version
__export__ = ["runtime_version"]
__export__ = ['runtime_version']

View File

@@ -171,24 +171,3 @@ test('MicroPython buffered NO error', async ({ page }) => {
const body = await page.evaluate(() => document.body.textContent.trim());
await expect(body).toBe('');
});
test('Pyodide media module', async ({ page }) => {
await page.context().grantPermissions(['camera', 'microphone']);
await page.context().addInitScript(() => {
const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices;
navigator.mediaDevices.enumerateDevices = async function() {
const realDevices = await originalEnumerateDevices.call(this);
if (!realDevices || realDevices.length === 0) {
return [
{ deviceId: 'camera1', groupId: 'group1', kind: 'videoinput', label: 'Simulated Camera' },
{ deviceId: 'mic1', groupId: 'group2', kind: 'audioinput', label: 'Simulated Microphone' }
];
}
return realDevices;
};
});
await page.goto('http://localhost:8080/tests/javascript/media.html');
await page.waitForSelector('html.media-ok', { timeout: 10000 });
const isSuccess = await page.evaluate(() => document.documentElement.classList.contains('media-ok'));
expect(isSuccess).toBe(true);
});

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../../../dist/core.css">
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<script type="mpy" src="index.py"></script>
</body>
</html>

View File

@@ -1,46 +0,0 @@
import os
from pyscript import RUNNING_IN_WORKER, fs
TEST = "implicit"
if TEST == "implicit":
await fs.mount("/persistent")
print(
(RUNNING_IN_WORKER and "Worker") or "Main",
os.listdir("/persistent"),
)
from random import random
with open("/persistent/random.txt", "w") as f:
f.write(str(random()))
await fs.sync("/persistent")
elif not RUNNING_IN_WORKER:
from pyscript import document
button = document.createElement("button")
button.textContent = "mount"
document.body.append(button)
async def mount(event):
try:
await fs.mount("/persistent")
print(os.listdir("/persistent"))
button.textContent = "unmount"
button.onclick = unmount
except:
import js
js.alert("unable to grant access")
async def unmount(event):
await fs.unmount("/persistent")
button.textContent = "mount"
button.onclick = mount
button.onclick = mount

View File

@@ -1,4 +1,4 @@
"""(c) https://github.com/ryanking13/pyodide-pygame-demo/blob/main/examples/aliens.html
""" (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.
@@ -22,21 +22,17 @@ Controls
* f key to toggle between fullscreen.
"""
import asyncio
import random
import os
import pathlib
import pyscript
# import basic pygame modules
import pygame
# see if we can load more than standard BMP
if not pygame.image.get_extended():
msg = "Sorry, extended image module required"
raise SystemExit(msg)
raise SystemExit("Sorry, extended image module required")
# game constants
@@ -50,15 +46,13 @@ SCORE = 0
main_dir = str(pathlib.Path(pygame.__file__).parent / "examples")
def load_image(file):
"""loads an image, prepares it for play"""
file = os.path.join(main_dir, "data", file)
try:
surface = pygame.image.load(file)
except pygame.error:
msg = f'Could not load image "{file}" {pygame.get_error()}'
raise SystemExit(msg)
raise SystemExit(f'Could not load image "{file}" {pygame.get_error()}')
return surface.convert()
@@ -68,7 +62,8 @@ def load_sound(file):
return None
file = os.path.join(main_dir, "data", file)
try:
return pygame.mixer.Sound(file)
sound = pygame.mixer.Sound(file)
return sound
except pygame.error:
print(f"Warning, unable to load, {file}")
return None
@@ -228,7 +223,7 @@ class Score(pygame.sprite.Sprite):
def update(self):
"""We only update the score in update() when it has changed."""
if self.lastscore != SCORE:
if SCORE != self.lastscore:
self.lastscore = SCORE
msg = "Score: %d" % SCORE
self.image = self.font.render(msg, 0, self.color)
@@ -297,7 +292,7 @@ async def main(winstyle=0):
# Create Some Starting Values
global score
alienreload = ALIEN_RELOAD
_clock = pygame.Clock()
clock = pygame.Clock()
# initialize our starting sprites
global SCORE
@@ -314,23 +309,24 @@ async def main(winstyle=0):
return
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
return
if event.type == pygame.KEYDOWN and event.key == pygame.K_f:
if not fullscreen:
print("Changing to FULLSCREEN")
screen_backup = screen.copy()
screen = pygame.display.set_mode(
SCREENRECT.size, winstyle | pygame.FULLSCREEN, bestdepth
)
screen.blit(screen_backup, (0, 0))
else:
print("Changing to windowed mode")
screen_backup = screen.copy()
screen = pygame.display.set_mode(
SCREENRECT.size, winstyle, bestdepth
)
screen.blit(screen_backup, (0, 0))
pygame.display.flip()
fullscreen = not fullscreen
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()
@@ -371,7 +367,7 @@ async def main(winstyle=0):
player.kill()
# See if shots hit the aliens.
for alien in pygame.sprite.groupcollide(aliens, shots, 1, 1):
for alien in pygame.sprite.groupcollide(aliens, shots, 1, 1).keys():
if pygame.mixer:
boom_sound.play()
Explosion(alien)
@@ -395,5 +391,4 @@ async def main(winstyle=0):
if pygame.mixer:
pygame.mixer.music.fadeout(1000)
main()

View File

@@ -8,7 +8,12 @@
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<script type="py-game" src="aliens.py" config="./config.toml"></script>
<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">

View File

@@ -1,20 +0,0 @@
from pyscript import config
MICROPYTHON = config["type"] == "mpy"
if MICROPYTHON:
def new(obj, *args, **kwargs):
return obj.new(*args, kwargs) if kwargs else obj.new(*args)
def call(obj, *args, **kwargs):
return obj(*args, kwargs) if kwargs else obj(*args)
else:
def new(obj, *args, **kwargs):
return obj.new(*args, **kwargs)
def call(obj, *args, **kwargs):
return obj(*args, **kwargs)
if not MICROPYTHON:
import pyodide_js
pyodide_js.setDebug(True)
from pyscript.ffi import to_js, create_proxy

View File

@@ -1,69 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Genuary</title>
<!-- Recommended meta tags -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<!-- PyScript CSS -->
<link rel="stylesheet" href="../../../dist/core.css">
<style>
body {
margin: 0;
overflow: hidden;
background-color: #4a315e;
color: white;
font-family: Inconsolata, Consolas, Monaco, Courier New;
}
.gutter {
background-color: #eee;
background-repeat: no-repeat;
background-position: 50%;
}
.gutter.gutter-vertical {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
cursor: row-resize;
}
py-terminal {
max-height: 7em;
max-width: calc(100vw - 90px);
}
#pyterm {
background-color: #191a1a;
}
#pyterm,
#threejs {
position: relative;
overflow: hidden;
}
</style>
<!-- This script tag bootstraps PyScript -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@v0.173.0/build/three.module.js"
}
}
</script>
<script type="module" src="../../../dist/core.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/split.js/1.6.5/split.min.js"></script>
</head>
<body>
<div id="stats"></div>
<div id="stats-off"></div>
<div class="split">
<div id="pyterm"></div>
<div id="threejs"></div>
</div>
<script type="py" src="./main.py" config="./pyscript.toml" async terminal></script>
</body>
</html>

View File

@@ -1,83 +0,0 @@
from dataclasses import dataclass, field
import sys
@dataclass
class BeatSync:
fft_res: int = field()
on_beat: bool = False
beat: int = -1
since_last_beat: float = sys.maxsize
_prev: int = 0
_count: int = 0
_bins: list[int] = field(default_factory=list)
_last_detection: float = -1.0
_threshold: int = 50
_diff: int = 40
_cooldown: float = 0.2
_highest: int = 0
def __post_init__(self):
self._bins = [int(13/16*self.fft_res/2)+17, int(13/16*self.fft_res/2)+18]
def reset(self):
self.beat = -1
self._prev = 0
self._count = 0
self._last_detection = -1.0
self.since_last_beat = sys.maxsize
# print('bs reset')
def update(self, data, running_time):
self._count += 1
self.since_last_beat = running_time - self._last_detection
d = sum(data[bin] for bin in self._bins)
if d < self._threshold:
self.on_beat = False
elif d - self._prev < self._diff:
self.on_beat = False
elif self.since_last_beat < self._cooldown:
self.on_beat = False
else:
self._last_detection = running_time
self.since_last_beat = 0
self.on_beat = True
self.beat += 1
self._prev = d
@dataclass
class FreqIntensity:
freq: float = field()
fft_res: int = field()
intensity: float = 0.0
intensity_slew: float = 0.0
scale_min: float = 0.0
scale_max: float = 350
max: float = 0.0
_sample_rate: int = 48000
_bin_indexes: list[int] = field(default_factory=list)
_harmonics: int = 8
_slew_factor: float = 0.8
def __post_init__(self):
self._bin_indexes = [
round((harmonic+1) * self.freq / self._sample_rate * self.fft_res / 2)
for harmonic in range(self._harmonics)
]
print(self._bin_indexes)
def update(self, data):
intensity = 0.0
for bin in range(self._harmonics):
intensity += data[self._bin_indexes[bin]]/(bin+1)
self.intensity = intensity
self.intensity_slew = self._slew_factor * self.intensity_slew + (1 - self._slew_factor) * intensity
self.max = max(intensity, self.max)
@property
def intensity_scaled(self):
raw = max(0, min(1.0, (self.intensity_slew - self.scale_min)/(self.scale_max - self.scale_min)))
return raw * raw

View File

@@ -1,189 +0,0 @@
import asyncio
from dataclasses import dataclass, field
from typing import Callable
from pyscript import document, window
from pyscript.js_modules import three as THREE
from pyscript.js_modules.stats_gl import default as StatsGL
from pyscript.js_modules import lsgeo, line2, linemat
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy
@dataclass
class SoundPlayer:
sound: THREE.Audio = field()
on_start: Callable[[], None] = field()
on_stop: Callable[[], None] = field(default=lambda: None)
_start_time: float = -1.0
def play(self):
self.sound.stop()
self.on_start()
self._start_time = self.sound.context.currentTime
self.sound.play()
def stop(self):
self.sound.stop()
self.on_stop()
self._start_time = -1.0
def toggle(self):
if self.sound.isPlaying:
self.stop()
else:
self.play()
@property
def running_time(self):
if self.sound.isPlaying:
return self.sound.context.currentTime - self._start_time
elif self._start_time != -1.0:
self.stop()
return 0.0
def get_renderer():
renderer = new(THREE.WebGLRenderer, antialias=True)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setClearColor(0xF5F0DC)
pyterms = list(document.getElementsByTagName("py-terminal"))
if pyterms:
pyterm = pyterms[0]
pyterm.parentNode.removeChild(pyterm)
document.getElementById("pyterm").appendChild(pyterm)
document.getElementById("threejs").appendChild(renderer.domElement)
initial = {0: "115px", 1: "calc(100vh - 120px)"}
@create_proxy
def split_element_style(dimension, size, gutter_size, index):
if index in initial:
result = {dimension: initial.pop(index)}
else:
result = {dimension: f"calc({int(size)}vh - {gutter_size}px)"}
return to_js(result)
call(
window.Split,
["#pyterm", "#threejs"],
direction="vertical",
elementStyle=split_element_style,
minSize=0,
maxSize=to_js([120, 10000]),
)
return renderer
def get_ortho_camera(view_size):
aspect_ratio = window.innerWidth / window.innerHeight
camera = new(
THREE.OrthographicCamera,
-view_size * aspect_ratio, # Left
view_size * aspect_ratio, # Right
view_size, # Top
-view_size, # Bottom
-view_size, # Near plane
view_size, # Far plane
)
camera.updateProjectionMatrix()
camera.position.set(0, 0, 0)
return camera
def get_loading_manager():
loading_mgr = new(THREE.LoadingManager)
ev = asyncio.Event()
@create_proxy
def on_start(url, itemsLoaded, itemsTotal):
print(f'[{itemsLoaded}/{itemsTotal}] Started loading file: {url}')
loading_mgr.onStart = on_start
@create_proxy
def on_progress(url, itemsLoaded, itemsTotal):
print(f'[{itemsLoaded}/{itemsTotal}] Loading file: {url}')
loading_mgr.onProgress = on_progress
@create_proxy
def on_error(url):
print(f'There was a problem loading {url}')
loading_mgr.onError = on_error
@create_proxy
def on_load():
print('Loading assets complete!')
ev.set()
loading_mgr.onLoad = on_load
return loading_mgr, ev
def get_perspective_camera():
aspect_ratio = window.innerWidth / window.innerHeight
camera = new(
THREE.PerspectiveCamera,
45, # fov
aspect_ratio,
0.25, # near plane
300, # far plane
)
camera.position.set(0, 0, 30)
return camera
def get_stats_gl(renderer):
stats = new(StatsGL, trackGPU=True, horizontal=False)
stats.init(renderer)
stats.dom.style.removeProperty("left")
stats.dom.style.right = "90px"
document.getElementById("stats").appendChild(stats.dom)
return stats
def bg_from_v(*vertices):
geometry = new(THREE.BufferGeometry)
vertices_f32a = new(Float32Array, vertices)
attr = new(THREE.Float32BufferAttribute, vertices_f32a, 3)
return geometry.setAttribute('position', attr)
def bg_from_p(*points):
buf = new(THREE.BufferGeometry)
buf.setFromPoints(
[new(THREE.Vector3, p[0], p[1], p[2]) for p in points]
)
return buf
def clear():
# toggle stats and terminal?
stats_style = document.getElementById("stats-off").style
if stats_style.display == "none":
# turn stuff back on
stats_style.removeProperty("display")
document.getElementById("pyterm").style.height = "115px"
document.getElementById("threejs").style.height = "calc(100vh - 120px)"
for e in document.getElementsByClassName("gutter"):
e.style.removeProperty("display")
for e in document.getElementsByClassName("xterm-helper-textarea"):
e.focus()
break
return
# no longer focus on xterm
document.activeElement.blur()
# hide stats
document.getElementById("stats-off").style.display = "none"
# hide pyterm and split gutter
document.getElementById("pyterm").style.height = "0vh"
document.getElementById("threejs").style.height = "100vh"
for e in document.getElementsByClassName("gutter"):
e.style.display = "none"
# hide ltk ad
for e in document.getElementsByClassName("ltk-built-with"):
e.style.display = "none"
# hide pyscript ad
for e in document.getElementsByTagName("div"):
style = e.getAttribute("style")
if style and style.startswith("z-index:999"):
e.style.display = "none"
for e in document.getElementsByTagName("svg"):
style = e.getAttribute("style")
if style and style.startswith("z-index:999"):
e.style.display = "none"

View File

@@ -1,285 +0,0 @@
print("Starting up...")
from array import array
import asyncio
import math
import time
from pyscript import document, window, PyWorker
from libthree import THREE, clear, SoundPlayer
from libthree import get_renderer, get_ortho_camera
from libthree import get_loading_manager, get_stats_gl
from libthree import lsgeo, line2, linemat, lsgeo
from libfft import BeatSync
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy
from js import Float32Array
scene = new(THREE.Scene)
view_size = 1
renderer = get_renderer()
camera = get_ortho_camera(view_size)
loading_mgr, loaded_event = get_loading_manager()
t_loader = new(THREE.TextureLoader, loading_mgr)
t_loader.setPath('assets/')
light = new(THREE.AmbientLight, 0xffffff, 1.0)
scene.add(light)
fft_res = 2048
audio_listener = new(THREE.AudioListener)
camera.add(audio_listener)
sound = new(THREE.Audio, audio_listener)
audio_loader = new(THREE.AudioLoader, loading_mgr)
analyser = new(THREE.AudioAnalyser, sound, fft_res)
@create_proxy
def on_audio_load(buffer):
sound.setBuffer(buffer)
sound.setVolume(0.9)
sound.setLoop(False)
audio_loader.load("assets/genuary25-18.m4a", on_audio_load)
spheres = new(THREE.Group)
scene.add(spheres)
line_basic_mat = new(
THREE.LineBasicMaterial,
color=0xffffff,
)
zero_mat = new(
linemat.LineMaterial,
color=0x662503,
linewidth=3,
)
other_mat = new(
linemat.LineMaterial,
color=0x662503,
linewidth=1.5,
)
grid_mat = new(
linemat.LineMaterial,
color=0x662503,
linewidth=1,
dashed=True,
dashScale=1,
dashSize=0.5,
gapSize=1,
dashOffset=0,
)
lines = [new(THREE.Group), new(THREE.Group)]
scene.add(lines[0])
scene.add(lines[1])
def draw_lines(line_coords, mat_name, spy=False):
if spy:
line_coords_f32a = new(Float32Array, line_coords.length)
_it = line_coords.items
for i in range(line_coords.length):
line_coords_f32a[i] = _it[i]
else:
line_coords_f32a = new(Float32Array, line_coords)
if mat_name == 'zero':
mat = zero_mat
elif mat_name == 'grid':
mat = grid_mat
else:
mat = other_mat
geo = new(THREE.BufferGeometry)
geo.setAttribute('position', new(THREE.BufferAttribute, line_coords_f32a, 3))
seg = new(THREE.LineSegments, geo, line_basic_mat)
lsg = new(lsgeo.LineSegmentsGeometry)
lsg.fromLineSegments(seg)
l1 = new(line2.Line2, lsg, mat)
l1.computeLineDistances()
l2 = new(line2.Line2, lsg, mat)
l2.computeLineDistances()
lines[0].add(l1)
lines[1].add(l2)
seg.geometry.dispose()
del geo
del seg
def drawing_done():
maybe_with_spy = "with SPy" if USE_SPY else "with pure Python"
print(f"Time elapsed computing {maybe_with_spy}:", time.time() - start_ts)
drawing_event.set()
grid_width = 0
grid_height = 0
scroll_offset = 0
def scale_lines(grid_ws=None, grid_hs=None, offset=None):
global grid_width, grid_height, scroll_offset
if grid_ws:
grid_width = grid_ws
else:
grid_ws = grid_width
if grid_hs:
grid_height = grid_hs
else:
grid_hs = grid_height
if offset:
scroll_offset = offset
else:
offset = scroll_offset
scale = 2.04/grid_hs
lines[0].scale.set(scale, scale, scale)
lines[1].scale.set(scale, scale, scale)
lines[0].position.set((offset - grid_ws/2) * scale, -grid_hs/2 * scale, 0)
lines[1].position.set((offset + grid_ws/2) * scale, -grid_hs/2 * scale, 0)
def append_p(lines, p1, p2):
lines.append(p1[0])
lines.append(p1[1])
lines.append(0)
lines.append(p2[0])
lines.append(p2[1])
lines.append(0)
def initial_calc():
grid_w = int(1920 * 4)
grid_h = 1080 * 2
grid_scale = 10
noise_factor = 500
grid_hs = int(grid_h/grid_scale)
grid_ws = int(grid_w/grid_scale)
crossfade_range = int(grid_ws/12.5)
def grid_lines():
lines = array("d")
grid_goal = 24
grid_size_i = int(round((grid_ws - crossfade_range) / grid_goal))
grid_actual = (grid_ws - crossfade_range) / grid_size_i
for i in range(0, grid_size_i):
x = i * grid_actual
append_p(lines, (x, 0), (x, grid_hs))
for y in range(0, grid_hs, grid_goal):
append_p(lines, (0, y), (grid_ws-crossfade_range, y))
return lines
import perlin
spy_perlin = perlin.lib
spy_perlin.init()
spy_perlin.seed(44)
scale_lines(grid_ws - crossfade_range, grid_hs)
print("Computing the height map")
spy_perlin.make_height_map(grid_ws, grid_hs)
spy_perlin.update_height_map(grid_ws, grid_hs, grid_scale / noise_factor, 0)
print("Cross-fading the height map")
spy_perlin.crossfade_height_map(grid_ws, grid_hs, crossfade_range)
print("Drawing grid")
draw_lines(grid_lines(), 'grid')
print("Marching squares")
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0), 'zero', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.3), 'positive', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.3), 'negative', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.45), 'positive', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.45), 'negative', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.6), 'positive', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.6), 'negative', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.8), 'negative', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.8), 'positive', spy=True)
drawing_done()
drawing_event = asyncio.Event()
start_ts = time.time()
USE_SPY = True
if USE_SPY:
initial_calc()
else:
worker = PyWorker("./worker.py", type="pyodide", configURL="./pyscript.toml")
worker.sync.draw_lines = draw_lines
worker.sync.drawing_done = drawing_done
worker.sync.scale_lines = scale_lines
worker.sync.print = print
@create_proxy
def on_tap(event):
clear()
player.toggle()
document.addEventListener("click", on_tap)
@create_proxy
def on_key_down(event):
element = document.activeElement
_class = element.getAttribute("class")
in_xterm = element.tagName != "BODY" and _class and "xterm" in _class
if event.code == "Backquote":
# Screenshot mode.
clear()
elif not in_xterm:
# Don't react to those bindings when typing code.
if event.code == "Space":
player.toggle()
document.addEventListener("keydown", on_key_down)
@create_proxy
def on_window_resize(event):
aspect_ratio = window.innerWidth / window.innerHeight
if camera.type == "OrthographicCamera":
camera.left = -view_size * aspect_ratio
camera.right = view_size * aspect_ratio
camera.top = view_size
camera.bottom = -view_size
camera.updateProjectionMatrix()
elif camera.type == "PerspectiveCamera":
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
else:
raise ValueError("Unknown camera type")
renderer.setSize(window.innerWidth, window.innerHeight)
scale_lines()
window.addEventListener("resize", on_window_resize)
@create_proxy
def animate(now=0.0):
data = analyser.getFrequencyData()#.to_py() in Pyodide
audio_now = player.running_time
bs.update(data, audio_now)
if grid_width:
offset = -((20 * audio_now) % grid_width)
scale_lines(offset=offset)
renderer.render(scene, camera)
stats_gl.update()
def reset():
global scroll_offset
bs.reset()
scale_lines()
def on_stop():
global scroll_offset
bs.reset()
scale_lines()
await loaded_event.wait()
stats_gl = get_stats_gl(renderer)
player = SoundPlayer(sound=sound, on_start=reset, on_stop=on_stop)
bs = BeatSync(fft_res=fft_res)
renderer.setAnimationLoop(animate)
print("Waiting for the contours...")
await drawing_event.wait()
print("Tap the map to start...")

View File

@@ -1,110 +0,0 @@
# Translated from https://github.com/josephg/noisejs.
from libthree import THREE
from multipyjs import new
class V3:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return f"V3({self.x}, {self.y}, {self.z})"
def dot2(self, x, y):
return self.x * x + self.y * y
def dot3(self, x, y, z):
return self.x * x + self.y * y + self.z * z
def to_js(self, scale=1.0):
return new(THREE.Vector3, self.x * scale, self.y * scale, self.z * scale)
PERM = [0] * 512
V3_P = [0] * 512 # assigned V3s in seed()
P = [151, 160, 137, 91, 90, 15,
131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23,
190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244,
102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196,
135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123,
5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42,
223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228,
251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107,
49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180]
V3_I = [V3(1, 1, 0), V3(-1, 1, 0), V3(1, -1, 0), V3(-1, -1, 0),
V3(1, 0, 1), V3(-1, 0, 1), V3(1, 0, -1), V3(-1, 0, -1),
V3(0, 1, 1), V3(0, -1, 1), V3(0, 1, -1), V3(0, -1, -1)]
def seed(s):
if isinstance(s, float) and 0.0 < s < 1.0:
s *= 65536
s = int(s)
if s < 256:
s |= s << 8
for i in range(256):
if i & 1:
v = P[i] ^ (s & 255)
else:
v = P[i] ^ ((s >> 8) & 255)
PERM[i] = PERM[i + 256] = v
V3_P[i] = V3_P[i + 256] = V3_I[v % 12]
seed(0)
def fade(t):
return t * t * t * (t * (t * 6 - 15) + 10)
def lerp(a, b, t):
return (1 - t) * a + t * b
def perlin3(x, y, z):
# grid cells
x_c = int(x)
y_c = int(y)
z_c = int(z)
# relative coords within the cell
x -= x_c
y -= y_c
z -= z_c
# wrap cells
x_c &= 255
y_c &= 255
z_c &= 255
# noise contributions to corners
n000 = V3_P[x_c + PERM[y_c + PERM[z_c]]].dot3(x, y, z)
n001 = V3_P[x_c + PERM[y_c + PERM[z_c + 1]]].dot3(x, y, z - 1)
n010 = V3_P[x_c + PERM[y_c + 1 + PERM[z_c]]].dot3(x, y - 1, z)
n011 = V3_P[x_c + PERM[y_c + 1 + PERM[z_c + 1]]].dot3(x, y - 1, z - 1)
n100 = V3_P[x_c + 1 + PERM[y_c + PERM[z_c]]].dot3(x - 1, y, z)
n101 = V3_P[x_c + 1 + PERM[y_c + PERM[z_c + 1]]].dot3(x - 1, y, z - 1)
n110 = V3_P[x_c + 1 + PERM[y_c + 1 + PERM[z_c]]].dot3(x - 1, y - 1, z)
n111 = V3_P[x_c + 1 + PERM[y_c + 1 + PERM[z_c + 1]]].dot3(x - 1, y - 1, z - 1)
# fade curve
u = fade(x)
v = fade(y)
w = fade(z)
# interpolation
return lerp(
lerp(lerp(n000, n100, u), lerp(n001, n101, u), w),
lerp(lerp(n010, n110, u), lerp(n011, n111, u), w),
v,
)
def curl2(x, y, z):
# https://www.bit-101.com/2017/2021/07/curl-noise/
delta = 0.01
n1 = perlin3(x + delta, y, z)
n2 = perlin3(x - delta, y, z)
cy = -(n1 - n2) / (delta * 2)
n1 = perlin3(x, y + delta, z)
n2 = perlin3(x, y - delta, z)
cx = -(n1 - n2) / (delta * 2)
print(n1, n2)
return V3(cx, cy, 0)

View File

@@ -1,16 +0,0 @@
name = "Marching Squares with SPy Copy Copy"
packages = [ "cffi", "./glue/perlin-0.0.0-cp312-cp312-pyodide_2024_0_wasm32.whl",]
[files]
"./libthree.py" = ""
"./libfft.py" = ""
"./perlin_py.py" = ""
"./worker.py" = ""
"./glue/multipyjs.py" = "./multipyjs.py"
[js_modules.main]
"https://cdn.jsdelivr.net/npm/three@v0.173.0/build/three.module.js" = "three"
"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/LineMaterial.js" = "linemat"
"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/Line2.js" = "line2"
"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/LineSegmentsGeometry.js" = "lsgeo"
"https://cdn.jsdelivr.net/npm/stats-gl@3.6.0/dist/main.js" = "stats_gl"

View File

@@ -1,141 +0,0 @@
from array import array
from pyscript import sync, window
from perlin_py import perlin3, seed
grid_w = int(1920 * 4)
grid_h = 1080 * 2
grid_scale = 10
noise_factor = 500
grid_hs = int(grid_h/grid_scale)
grid_ws = int(grid_w/grid_scale)
crossfade_range = int(grid_ws/12.5)
height_map = array("d", [0.0] * (grid_hs * grid_ws))
edge_table = [
(), # 0
((3, 2),), # 1
((2, 1),), # 2
((3, 1),), # 3
((0, 1),), # 4
((0, 3), (1, 2)), # 5 (ambiguous)
((0, 2),), # 6
((0, 3),), # 7
((0, 3),), # 8
((0, 2),), # 9
((0, 1), (2, 3)), # 10 (ambiguous)
((0, 1),), # 11
((3, 1),), # 12
((2, 1),), # 13
((3, 2),), # 14
(), # 15
]
def update_height_map(z):
i = 0
for y in range(0, grid_h, grid_scale):
for x in range(0, grid_w, grid_scale):
# 3 octaves of noise
n = perlin3(x/noise_factor, y/noise_factor, z)
n += 0.50 * perlin3(2*x/noise_factor, 2*y/noise_factor, z)
n += 0.25 * perlin3(4*x/noise_factor, 4*y/noise_factor, z)
height_map[i] = n
i += 1
def crossfade_height_map():
for y in range(grid_hs):
for x in range(crossfade_range):
pos_i = y*grid_ws + x
neg_i = y*grid_ws + grid_ws - crossfade_range + x
weight = x/crossfade_range
old_pos = height_map[pos_i]
old_neg = height_map[neg_i]
height_map[neg_i] = height_map[pos_i] = weight * old_pos + (1.0 - weight) * old_neg
def _crossfade_height_map():
for y in range(grid_hs):
for x in range(crossfade_range):
pos_i = y*grid_ws + x
neg_i = y*grid_ws + grid_ws - x - 1
old_pos = height_map[pos_i]
old_neg = height_map[neg_i]
weight = 0.5 - x/crossfade_range/2
height_map[pos_i] = (1.0 - weight) * old_pos + weight * old_neg
height_map[neg_i] = (1.0 - weight) * old_neg + weight * old_pos
def interpolate(sq_threshold, v1, v2):
if v1 == v2:
return v1
return (sq_threshold - v1) / (v2 - v1)
stats = {'maxx': 0, 'maxy': 0, 'minx': 0, 'miny': 0}
def append_p(lines, p1, p2):
lines.append(p1[0])
lines.append(p1[1])
lines.append(0)
lines.append(p2[0])
lines.append(p2[1])
lines.append(0)
stats['maxy'] = max(p1[1], p2[1], stats['maxy'])
stats['miny'] = min(p1[1], p2[1], stats['miny'])
stats['maxx'] = max(p1[0], p2[0], stats['maxx'])
stats['minx'] = min(p1[0], p2[0], stats['minx'])
def marching_squares(height_map, sq_threshold):
lines = array("d")
for y in range(grid_hs-1):
for x in range(grid_ws-1): #cf
tl = height_map[y*grid_ws + x]
tr = height_map[y*grid_ws + x+1]
bl = height_map[(y+1)*grid_ws + x]
br = height_map[(y+1)*grid_ws + x+1]
sq_idx = 0
if tl > sq_threshold:
sq_idx |= 8
if tr > sq_threshold:
sq_idx |= 4
if br > sq_threshold:
sq_idx |= 2
if bl > sq_threshold:
sq_idx |= 1
edge_points = [
(x + interpolate(sq_threshold, tl, tr), y),
(x + 1, y + interpolate(sq_threshold, tr, br)),
(x + interpolate(sq_threshold, bl, br), y + 1),
(x, y + interpolate(sq_threshold, tl, bl)),
]
for a, b in edge_table[sq_idx]:
append_p(lines, edge_points[a], edge_points[b])
return lines
def grid_lines():
lines = array("d")
for x in range(0, grid_ws - crossfade_range, 26):
append_p(lines, (x, 0), (x, grid_hs))
for y in range(0, grid_hs, 24):
append_p(lines, (0, y), (grid_ws-crossfade_range, y))
return lines
seed(44)
sync.scale_lines(grid_ws - crossfade_range, grid_hs)
sync.print("Computing the height map")
update_height_map(0)
sync.print("Cross-fading the height map")
crossfade_height_map()
sync.draw_lines(grid_lines(), 'grid')
sync.draw_lines(marching_squares(height_map, 0), 'zero')
sync.draw_lines(marching_squares(height_map, 0.3), 'positive')
sync.draw_lines(marching_squares(height_map, -0.3), 'negative')
sync.draw_lines(marching_squares(height_map, 0.45), 'positive')
sync.draw_lines(marching_squares(height_map, -0.45), 'negative')
sync.draw_lines(marching_squares(height_map, 0.6), 'positive')
sync.draw_lines(marching_squares(height_map, -0.6), 'negative')
sync.draw_lines(marching_squares(height_map, -0.8), 'negative')
sync.draw_lines(marching_squares(height_map, 0.8), 'positive')
print(stats)
sync.drawing_done()

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="../../../dist/core.css">
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<div id="status">Status:</div>
<canvas id="canvas" width="200" height="200"></canvas>
<script type="py-game" src="./main.py" config="./pyscript.toml"></script>
</body>
</html>

View File

@@ -1,34 +0,0 @@
import sys
print("Starting test...")
# Try NumPy
try:
import numpy as np
arr = np.array([1, 2, 3])
print(f"NumPy works: {arr.mean()}")
except Exception as e:
print(f"NumPy error: {e}")
# Try PyGame without NumPy first
try:
print("Testing PyGame...")
import pygame
screen = pygame.display.set_mode((200, 200))
screen.fill((255, 0, 0)) # Fill with red
pygame.display.flip()
print("PyGame works!")
except Exception as e:
print(f"PyGame error: {e}")
# Now try PyGame with NumPy
try:
print("Testing PyGame+NumPy...")
color_array = np.random.randint(0, 255, size=(50, 50, 3), dtype=np.uint8)
surface = pygame.surfarray.make_surface(color_array)
screen.blit(surface, (75, 75))
pygame.display.flip()
print("PyGame+NumPy integration works!")
except Exception as e:
print(f"PyGame+NumPy integration error: {e}")
print("Test completed")

View File

@@ -1,2 +0,0 @@
name = "PyGame Numpy Minimal Example Copy"
packages = [ "numpy", ]

View File

@@ -1,21 +0,0 @@
import { test, expect } from '@playwright/test';
const timeout = 60 * 1000;
test.setTimeout(timeout);
test('Python unit tests - MicroPython on MAIN thread', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor({ timeout }); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});
test('Python unit tests - Pyodide on MAIN thread', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?type=py');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor({ timeout }); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});

View File

@@ -0,0 +1,35 @@
import { test, expect } from '@playwright/test';
test.setTimeout(120 * 1000);
test('Python unit tests - MicroPython on MAIN thread', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor(); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});
test('Python unit tests - Pyodide on MAIN thread', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?type=py');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor(); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});
test('Python unit tests - MicroPython on WORKER', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?worker');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor(); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});
test('Python unit tests - Pyodide on WORKER', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?type=py&worker');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor(); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});

View File

@@ -1,21 +0,0 @@
import { test, expect } from '@playwright/test';
const timeout = 120 * 1000;
test.setTimeout(timeout);
test('Python unit tests - MicroPython on WORKER', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?worker');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor({ timeout }); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});
test('Python unit tests - Pyodide on WORKER', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?type=py&worker');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor({ timeout }); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});

View File

@@ -8,7 +8,6 @@
"./tests/test_fetch.py": "tests/test_fetch.py",
"./tests/test_ffi.py": "tests/test_ffi.py",
"./tests/test_js_modules.py": "tests/test_js_modules.py",
"./tests/test_media.py": "tests/test_media.py",
"./tests/test_storage.py": "tests/test_storage.py",
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
"./tests/test_web.py": "tests/test_web.py",

View File

@@ -7,7 +7,6 @@
"./tests/test_document.py": "tests/test_document.py",
"./tests/test_fetch.py": "tests/test_fetch.py",
"./tests/test_ffi.py": "tests/test_ffi.py",
"./tests/test_media.py": "tests/test_media.py",
"./tests/test_js_modules.py": "tests/test_js_modules.py",
"./tests/test_storage.py": "tests/test_storage.py",
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",

View File

@@ -13,7 +13,10 @@ def test_current_target():
"""
expected = "py-0"
if is_micropython:
expected = "mpy-w0-target" if RUNNING_IN_WORKER else "mpy-0"
if RUNNING_IN_WORKER:
expected = "mpy-w0-target"
else:
expected = "mpy-0"
elif RUNNING_IN_WORKER:
expected = "py-w0-target"
assert current_target() == expected, f"Expected {expected} got {current_target()}"

View File

@@ -256,7 +256,7 @@ async def test_image_display():
"""
Check an image is displayed correctly.
"""
_mpl = await py_import("matplotlib")
mpl = await py_import("matplotlib")
import matplotlib.pyplot as plt
xpoints = [3, 6, 9]

View File

@@ -22,17 +22,6 @@ def teardown():
container.innerHTML = ""
def test_event_no_result():
"""
If an event has not been triggered with a result, accessing the result
parameter raises a ValueError.
"""
event = Event()
with upytest.raises(ValueError) as e:
event.result
assert str(e.exception) == "Event has not been triggered yet. No result available."
def test_event_add_listener():
"""
Adding a listener to an event should add it to the list of listeners. It
@@ -46,52 +35,6 @@ def test_event_add_listener():
assert listener in event._listeners # The item is the expected listener.
def test_event_add_invalid_listener():
"""
Adding an invalid listener should raise a ValueError.
"""
event = Event()
with upytest.raises(ValueError) as e:
event.add_listener("invalid")
assert str(e.exception) == "Listener must be callable or awaitable."
def test_event_add_listener_triggered():
"""
Adding a listener to an event that has already been triggered should call
the listener immediately with the result.
"""
event = Event()
counter = 0
def listener(x):
nonlocal counter
counter += 1
assert x == "ok"
event.trigger("ok")
event.add_listener(listener)
assert counter == 1 # The listener has been triggered with the expected result.
def test_event_add_listener_multiple_times():
"""
Adding the same listener multiple times should not call it multiple times.
"""
event = Event()
counter = 0
def listener(x):
nonlocal counter
counter += 1
assert x == "ok"
event.add_listener(listener)
event.add_listener(listener)
event.trigger("ok")
assert counter == 1 # The listener has been triggered only once.
def test_event_remove_listener():
"""
Removing a listener from an event should remove it from the list of

View File

@@ -1,87 +0,0 @@
""""
Tests for the PyScript media module.
"""
from pyscript import media
import upytest
from pyscript import media
@upytest.skip(
"Uses Pyodide-specific to_js function in MicroPython",
skip_when=upytest.is_micropython,
)
async def test_device_enumeration():
"""Test enumerating media devices."""
devices = await media.list_devices()
assert isinstance(devices, list), "list_devices should return a list"
# If devices are found, verify they have the expected functionality
if devices:
device = devices[0]
# Test real device properties exist (but don't assert on their values)
# Browser security might restrict actual values until permissions are granted
assert hasattr(device, "id"), "Device should have id property"
assert hasattr(device, "kind"), "Device should have kind property"
assert device.kind in [
"videoinput",
"audioinput",
"audiooutput",
], f"Device should have a valid kind, got: {device.kind}"
# Verify dictionary access works with actual device
assert (
device["id"] == device.id
), "Dictionary access should match property access"
assert (
device["kind"] == device.kind
), "Dictionary access should match property access"
@upytest.skip("Waiting on a bug-fix in MicroPython, for this test to work.", skip_when=upytest.is_micropython)
async def test_video_stream_acquisition():
"""Test video stream."""
try:
# Load a video stream
stream = await media.Device.load(video=True)
# Verify we get a real stream with expected properties
assert hasattr(stream, "active"), "Stream should have active property"
# Check for video tracks, but don't fail if permissions aren't granted
if stream._dom_element and hasattr(stream._dom_element, "getVideoTracks"):
tracks = stream._dom_element.getVideoTracks()
if tracks.length > 0:
assert True, "Video stream has video tracks"
except Exception as e:
# If the browser blocks access, the test should still pass
# This is because we're testing the API works, not that permissions are granted
assert (
True
), f"Stream acquisition attempted but may require permissions: {str(e)}"
@upytest.skip("Waiting on a bug-fix in MicroPython, for this test to work.", skip_when=upytest.is_micropython)
async def test_custom_video_constraints():
"""Test loading video with custom constraints."""
try:
# Define custom constraints
constraints = {"width": 640, "height": 480}
# Load stream with custom constraints
stream = await media.Device.load(video=constraints)
# Basic stream property check
assert hasattr(stream, "active"), "Stream should have active property"
# Check for tracks only if we have access
if stream._dom_element and hasattr(stream._dom_element, "getVideoTracks"):
tracks = stream._dom_element.getVideoTracks()
if tracks.length > 0 and hasattr(tracks[0], "getSettings"):
# Settings verification is optional - browsers may handle constraints differently
pass
except Exception as e:
# If the browser blocks access, test that the API structure works
assert True, f"Custom constraint test attempted: {str(e)}"

View File

@@ -65,6 +65,7 @@ async def test_storage_types():
assert test_store["string"] == "hello"
assert isinstance(test_store["string"], str)
assert test_store["none"] is None
assert isinstance(test_store["none"], type(None))
assert test_store["list"] == [1, 2, 3]
assert isinstance(test_store["list"], list)
assert test_store["dict"] == {"a": 1, "b": 2}

View File

@@ -248,7 +248,7 @@ class TestCollection:
def test_iter_eq_children(self):
elements = web.page.find(".multi-elems")
assert list(elements) == list(elements.elements)
assert [el for el in elements] == [el for el in elements.elements]
assert len(elements) == 3
def test_slices(self):
@@ -427,18 +427,18 @@ class TestInput:
class TestSelect:
def test_select_options_iter(self):
select = web.page.find("#test_select_element_w_options")[0]
select = web.page.find(f"#test_select_element_w_options")[0]
for i, option in enumerate(select.options, 1):
assert option.value == f"{i}"
assert option.innerHTML == f"Option {i}"
def test_select_options_len(self):
select = web.page.find("#test_select_element_w_options")[0]
select = web.page.find(f"#test_select_element_w_options")[0]
assert len(select.options) == 2
def test_select_options_clear(self):
select = web.page.find("#test_select_element_to_clear")[0]
select = web.page.find(f"#test_select_element_to_clear")[0]
assert len(select.options) == 3
select.options.clear()
@@ -447,7 +447,7 @@ class TestSelect:
def test_select_element_add(self):
# GIVEN the existing select element with no options
select = web.page.find("#test_select_element")[0]
select = web.page.find(f"#test_select_element")[0]
# EXPECT the select element to have no options
assert len(select.options) == 0
@@ -498,14 +498,20 @@ class TestSelect:
# EXPECT the middle option to have the value and html we passed in
assert select.options[0].value == "1"
assert select.options[0].innerHTML == "Option 1"
assert select.options[0].selected == select.options[0]._dom_element.selected
assert select.options[0].selected is False
assert (
select.options[0].selected
== select.options[0]._dom_element.selected
== False
)
assert select.options[1].value == "2"
assert select.options[1].innerHTML == "Option 2"
assert select.options[2].value == "3"
assert select.options[2].innerHTML == "Option 3"
assert select.options[2].selected == select.options[2]._dom_element.selected
assert select.options[2].selected is True
assert (
select.options[2].selected
== select.options[2]._dom_element.selected
== True
)
assert select.options[3].value == ""
assert select.options[3].innerHTML == ""
@@ -532,7 +538,7 @@ class TestSelect:
def test_select_options_remove(self):
# GIVEN the existing select element with 3 options
select = web.page.find("#test_select_element_to_remove")[0]
select = web.page.find(f"#test_select_element_to_remove")[0]
# EXPECT the select element to have 3 options
assert len(select.options) == 4
@@ -554,7 +560,7 @@ class TestSelect:
def test_select_get_selected_option(self):
# GIVEN the existing select element with one selected option
select = web.page.find("#test_select_element_w_options")[0]
select = web.page.find(f"#test_select_element_w_options")[0]
# WHEN we get the selected option
selected_option = select.options.selected
@@ -562,8 +568,7 @@ class TestSelect:
# EXPECT the selected option to be correct
assert selected_option.value == "2"
assert selected_option.innerHTML == "Option 2"
assert selected_option.selected == selected_option._dom_element.selected
assert selected_option.selected is True
assert selected_option.selected == selected_option._dom_element.selected == True
class TestElements:
@@ -620,8 +625,7 @@ class TestElements:
el = klass(*args, **kwargs)
container.append(el)
except Exception as e:
msg = f"Failed to create element {el_type}: {e}"
raise AssertionError(msg)
assert False, f"Failed to create element {el_type}: {e}"
# Let's keep the tag in 2 variables, one for the selector and another to
# check the return tag from the selector

View File

@@ -3,12 +3,10 @@ Exercise the pyscript.Websocket class.
"""
import asyncio
import upytest
from pyscript import WebSocket
@upytest.skip("Websocket tests are disabled.")
async def test_websocket_with_attributes():
"""
Event handlers assigned via object attributes.
@@ -54,7 +52,6 @@ async def test_websocket_with_attributes():
assert closed_flag is True
@upytest.skip("Websocket tests are disabled.")
async def test_websocket_with_init():
"""
Event handlers assigned via __init__ arguments.

View File

@@ -1,7 +1,2 @@
export function configDetails(config: string, type: string | null): {
json: boolean;
toml: boolean;
text: string;
};
export const configs: Map<any, any>;
export function relative_url(url: any, base?: string): string;

8
core/types/fs.d.ts vendored
View File

@@ -1,8 +0,0 @@
export const NAMESPACE: "@pyscript.fs";
export const ERROR: "storage permissions not granted";
export const idb: any;
export function getFileSystemDirectoryHandle(options: {
id?: string;
mode?: "read" | "readwrite";
hint?: "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
}): Promise<FileSystemDirectoryHandle>;

View File

@@ -4,7 +4,6 @@ declare const _default: {
donkey: () => Promise<typeof import("./plugins/donkey.js")>;
error: () => Promise<typeof import("./plugins/error.js")>;
"py-editor": () => Promise<typeof import("./plugins/py-editor.js")>;
"py-game": () => Promise<typeof import("./plugins/py-game.js")>;
"py-terminal": () => Promise<typeof import("./plugins/py-terminal.js")>;
};
export default _default;

View File

@@ -1 +0,0 @@
export {};

View File

@@ -6,7 +6,6 @@ declare namespace _default {
"fetch.py": string;
"ffi.py": string;
"flatted.py": string;
"fs.py": string;
"magic_js.py": string;
"media.py": string;
"storage.py": string;

11
core/types/sync.d.ts vendored
View File

@@ -5,16 +5,5 @@ declare namespace _default {
* @param {number} seconds The number of seconds to sleep.
*/
function sleep(seconds: number): Promise<any>;
/**
* Ask a user action via dialog and returns the directory handler once granted.
* @param {string} uid
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
* @returns {boolean}
*/
function storeFSHandler(uid: string, options?: {
id?: string;
mode?: "read" | "readwrite";
hint?: "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
}): boolean;
}
export default _default;

View File

@@ -1,9 +1,3 @@
[tool.codespell]
ignore-words-list = "afterall"
skip = "*.js,*.json"
[tool.ruff]
line-length = 114
lint.select = ["C4", "C90", "E", "EM", "F", "PIE", "PYI", "PLC", "Q", "RET", "W"]
lint.ignore = ["E402", "E722", "E731", "E741", "F401", "F704", "F811", "F821"]
lint.mccabe.max-complexity = 27