Compare commits

...

17 Commits

Author SHA1 Message Date
Fabio Pliger
bcaab0eb93 PyDom compatibility with MicroPython (#1954)
* fix pydom example

* fix the pydom test example to use a python syntax that works with MicroPython by replacing datetime

* add note about capturing errors importing when

* patch event_handler to handle compat with micropython

* turn pyweb into a package and remove hack to make pydom a sort of module with an ugly hack

* add pydom example using micropython

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

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

* fix select element test

* change pydom test page to let pytest tests load it properly

* add missing folders to test dev server so it can run examples in the manual tests folder

* add pydom tests to the test suite as integration tests

* lint

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

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

* improve fixes in event_handling

* change when decorator to actually dynamically fail in micropython and support handlers with or without arguments

* simplify when decorator code

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

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

* add type declaration back for the MP use case

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

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

* removed code to access pydom get index as I can't think of any proper use case

* remove old commented hack to replace pydom module with class

* fix examples title

* precommit checks

* [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-01-30 11:30:16 -08:00
Andrea Giammarchi
3ff0f84391 Update polyscript + coincident to their latest (#1958) 2024-01-30 12:31:44 +01:00
Andrea Giammarchi
2b411fc635 Update Polyscript to its latest w/ experimental (#1955) 2024-01-29 12:00:37 +01:00
Fabio Pliger
2128572ce5 pyweb camera support (#1901)
* add media module

* add Device class to media

* add camera test example

* add snap, download and other convenience methods

* load devices automagically

* add draw method to canvas

* add docstring for download

* add docstrings to draw method

* add docstrings to snap

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

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

* load devices as soon as the page loads

* solve conflict

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

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

* remove display calls listing devices in camera example

* fix typos and other small errors

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

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

* fix typo in docstring

* fix error message typo

* replace setAttribute on JS properties with accessors

* remove debug statement

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

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

* add docstrings

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

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

* add docstrings to camera example

* [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-01-26 14:33:02 -08:00
Andrea Giammarchi
63f2453091 Fix #1946 - Do not hold while bootstrapping (#1953) 2024-01-26 15:04:02 +01:00
Andrea Giammarchi
f6470dcad5 Multiple Worker based Terminals (#1948)
Multiple Worker based Terminals
2024-01-24 17:33:55 +01:00
Andrea Giammarchi
a9717afeb7 updated Pyodide to 0.25.0 (#1949) 2024-01-24 13:19:33 +01:00
Andrea Giammarchi
cea52b4334 Adding __terminal__ reference on terminal scripts (#1947) 2024-01-22 15:34:36 +01:00
Andrea Giammarchi
7ad7f0abfb Fix #1943 - Updated Polyscript with configURL (#1944) 2024-01-17 16:15:51 +01:00
Andrea Giammarchi
1efd73af8f Instrumented the io.stderr too when terminal exists (#1942) 2024-01-16 19:05:15 +01:00
Andrea Giammarchi
1e7fb9af44 Fix #1940 - Handle Main Xterm as tty too (#1941) 2024-01-15 17:29:28 +01:00
Fabio Pliger
154e00d320 Add correct copyright and attribution to main license file (#1937)
* add correct copyright and attribution to main license file

* [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-01-13 22:32:21 +01:00
Andrea Giammarchi
0f788fa284 Fix #1899 - Expose pyscript.js_modules as module (#1902)
* Fix #1899 - Expose pyscript.js_modules as module

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

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

* Fix #1899 - Make import as smooth as in polyscript

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-03 17:16:51 +01:00
Andrea Giammarchi
355866a1f1 PyTerminal - expose the reference through the element (#1921)
* PyTerminal - expose the reference through the element

* [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-01-03 16:24:38 +01:00
pre-commit-ci[bot]
6eca06ac0b [pre-commit.ci] pre-commit autoupdate (#1844)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0)
- [github.com/psf/black: 23.1.0 → 23.11.0](https://github.com/psf/black/compare/23.1.0...23.11.0)
- [github.com/codespell-project/codespell: v2.2.4 → v2.2.6](https://github.com/codespell-project/codespell/compare/v2.2.4...v2.2.6)

* fixed typo in comment

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2023-12-19 14:18:27 +01:00
Andrea Giammarchi
a4aef0b530 Fix CI VS local env inconsistencies (#1892)
* Fix make fmt changing Python files

* [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>
2023-12-19 13:52:45 +01:00
Andrea Giammarchi
136e95498f Reduce conflicts on multiple custom scripts (#1897) 2023-12-14 18:32:46 +01:00
41 changed files with 1166 additions and 464 deletions

View File

@@ -7,7 +7,7 @@ ci:
default_stages: [commit] default_stages: [commit]
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: check-builtin-literals - id: check-builtin-literals
- id: check-case-conflict - id: check-case-conflict
@@ -25,13 +25,13 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.1.0 rev: 23.11.0
hooks: hooks:
- id: black - id: black
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.2.4 rev: v2.2.6
hooks: hooks:
- id: codespell # See 'pyproject.toml' for args - id: codespell # See 'pyproject.toml' for args
exclude: \.js\.map$ exclude: \.js\.map$

View File

@@ -186,7 +186,11 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright (c) 2022-present, PyScript Development Team
Originated at Anaconda, Inc. in 2022
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.3.9", "version": "0.3.23",
"type": "module", "type": "module",
"description": "PyScript", "description": "PyScript",
"module": "./index.js", "module": "./index.js",
@@ -42,18 +42,18 @@
"dependencies": { "dependencies": {
"@ungap/with-resolvers": "^0.1.0", "@ungap/with-resolvers": "^0.1.0",
"basic-devtools": "^0.1.6", "basic-devtools": "^0.1.6",
"polyscript": "^0.6.2", "polyscript": "^0.6.18",
"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.3.2", "@codemirror/commands": "^6.3.3",
"@codemirror/lang-python": "^6.1.3", "@codemirror/lang-python": "^6.1.3",
"@codemirror/language": "^6.9.3", "@codemirror/language": "^6.10.0",
"@codemirror/state": "^6.3.3", "@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.22.1", "@codemirror/view": "^6.23.1",
"@playwright/test": "^1.40.1", "@playwright/test": "^1.41.1",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
@@ -61,8 +61,8 @@
"@xterm/addon-fit": "^0.9.0-beta.1", "@xterm/addon-fit": "^0.9.0-beta.1",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"eslint": "^8.55.0", "eslint": "^8.56.0",
"rollup": "^4.6.1", "rollup": "^4.9.6",
"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.4.3", "static-handler": "^0.4.3",

View File

@@ -63,6 +63,9 @@ for (const [TYPE] of TYPES) {
/** @type {Error | undefined} The error thrown when parsing the PyScript config, if any.*/ /** @type {Error | undefined} The error thrown when parsing the PyScript config, if any.*/
let error; let error;
/** @type {string | undefined} The `configURL` field to normalize all config operations as opposite of guessing it once resolved */
let configURL;
let config, let config,
type, type,
pyElement, pyElement,
@@ -105,6 +108,7 @@ for (const [TYPE] of TYPES) {
if (!error && config) { if (!error && config) {
try { try {
const { json, toml, text, url } = await configDetails(config, type); const { json, toml, text, url } = await configDetails(config, type);
if (url) configURL = new URL(url, location.href).href;
config = text; config = text;
if (json || type === "json") { if (json || type === "json") {
try { try {
@@ -146,7 +150,7 @@ for (const [TYPE] of TYPES) {
// assign plugins as Promise.all only if needed // assign plugins as Promise.all only if needed
plugins = Promise.all(toBeAwaited); plugins = Promise.all(toBeAwaited);
configs.set(TYPE, { config: parsed, plugins, error }); configs.set(TYPE, { config: parsed, configURL, plugins, error });
} }
export default configs; export default configs;

View File

@@ -26,33 +26,9 @@ import { ErrorCode } from "./exceptions.js";
import { robustFetch as fetch, getText } from "./fetch.js"; import { robustFetch as fetch, getText } from "./fetch.js";
import { hooks, main, worker, codeFor, createFunction } from "./hooks.js"; import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
// allows lazy element features on code evaluation
let currentElement;
// generic helper to disambiguate between custom element and script // generic helper to disambiguate between custom element and script
const isScript = ({ tagName }) => tagName === "SCRIPT"; const isScript = ({ tagName }) => tagName === "SCRIPT";
let shouldRegister = true;
const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
// automatically use the pyscript stderr (when/if defined)
// this defaults to console.error
function PyWorker(...args) {
const worker = $XWorker(...args);
worker.onerror = ({ error }) => io.stderr(error);
return worker;
}
// enrich the Python env with some JS utility for main
interpreter.registerJsModule("_pyscript", {
PyWorker,
get target() {
return isScript(currentElement)
? currentElement.target.id
: currentElement.id;
},
});
};
// avoid multiple initialization of the same library // avoid multiple initialization of the same library
const [ const [
{ {
@@ -88,7 +64,7 @@ for (const [TYPE, interpreter] of TYPES) {
else dispatch(element, TYPE, "done"); else dispatch(element, TYPE, "done");
}; };
const { config, plugins, error } = configs.get(TYPE); const { config, configURL, plugins, error } = configs.get(TYPE);
// create a unique identifier when/if needed // create a unique identifier when/if needed
let id = 0; let id = 0;
@@ -118,6 +94,36 @@ for (const [TYPE, interpreter] of TYPES) {
return code; return code;
}; };
// register once any interpreter
let alreadyRegistered = false;
// allows lazy element features on code evaluation
let currentElement;
const registerModule = ({ XWorker, interpreter, io }) => {
// avoid multiple registration of the same interpreter
if (alreadyRegistered) return;
alreadyRegistered = true;
// automatically use the pyscript stderr (when/if defined)
// this defaults to console.error
function PyWorker(...args) {
const worker = XWorker(...args);
worker.onerror = ({ error }) => io.stderr(error);
return worker;
}
// enrich the Python env with some JS utility for main
interpreter.registerJsModule("_pyscript", {
PyWorker,
get target() {
return isScript(currentElement)
? currentElement.target.id
: currentElement.id;
},
});
};
// define the module as both `<script type="py">` and `<py-script>` // define the module as both `<script type="py">` and `<py-script>`
// but only if the config didn't throw an error // but only if the config didn't throw an error
if (!error) { if (!error) {
@@ -133,10 +139,7 @@ for (const [TYPE, interpreter] of TYPES) {
main: { main: {
...codeFor(main), ...codeFor(main),
async onReady(wrap, element) { async onReady(wrap, element) {
if (shouldRegister) {
shouldRegister = false;
registerModule(wrap); registerModule(wrap);
}
// allows plugins to do whatever they want with the element // allows plugins to do whatever they want with the element
// before regular stuff happens in here // before regular stuff happens in here
@@ -256,6 +259,7 @@ for (const [TYPE, interpreter] of TYPES) {
define(TYPE, { define(TYPE, {
config, config,
configURL,
interpreter, interpreter,
hooks, hooks,
env: `${TYPE}-script`, env: `${TYPE}-script`,
@@ -320,7 +324,7 @@ for (const [TYPE, interpreter] of TYPES) {
function PyWorker(file, options) { function PyWorker(file, options) {
const hooks = hooked.get("py"); const hooks = hooked.get("py");
// this propagates pyscript worker hooks without needing a pyscript // this propagates pyscript worker hooks without needing a pyscript
// bootstrap + it passes arguments and enforces `pyodide` // bootstrap + it passes arguments and it defaults to `pyodide`
// as the interpreter to use in the worker, as all hooks assume that // as the interpreter to use in the worker, as all hooks assume that
// and as `pyodide` is the only default interpreter that can deal with // and as `pyodide` is the only default interpreter that can deal with
// all the features we need to deliver pyscript out there. // all the features we need to deliver pyscript out there.

View File

@@ -200,6 +200,9 @@ const init = async (script, type, interpreter) => {
// avoid too greedy MutationObserver operations at distance // avoid too greedy MutationObserver operations at distance
let timeout = 0; let timeout = 0;
// avoid delayed initialization
let queue = Promise.resolve();
// reset interval value then check for new scripts // reset interval value then check for new scripts
const resetTimeout = () => { const resetTimeout = () => {
timeout = 0; timeout = 0;
@@ -213,11 +216,14 @@ const pyEditor = async () => {
for (const [type, interpreter] of TYPES) { for (const [type, interpreter] of TYPES) {
const selector = `script[type="${type}-editor"]`; const selector = `script[type="${type}-editor"]`;
for (const script of document.querySelectorAll(selector)) { for (const script of document.querySelectorAll(selector)) {
// avoid any further bootstrap // avoid any further bootstrap by changing the type as active
script.type += "-active"; script.type += "-active";
await init(script, type, interpreter); // don't await in here or multiple calls might happen
// while the first script is being initialized
queue = queue.then(() => init(script, type, interpreter));
} }
} }
return queue;
}; };
new MutationObserver(pyEditor).observe(document, { new MutationObserver(pyEditor).observe(document, {

View File

@@ -1,6 +1,7 @@
// PyScript py-terminal plugin // PyScript py-terminal plugin
import { TYPES, hooks } from "../core.js"; import { TYPES, hooks } from "../core.js";
import { notify } from "./error.js"; import { notify } from "./error.js";
import { defineProperty } from "polyscript/exports";
const SELECTOR = [...TYPES.keys()] const SELECTOR = [...TYPES.keys()]
.map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`) .map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`)
@@ -13,31 +14,73 @@ const notifyAndThrow = (message) => {
throw new Error(message); throw new Error(message);
}; };
const notParsedYet = (script) => !bootstrapped.has(script);
const onceOnMain = ({ attributes: { worker } }) => !worker;
const bootstrapped = new WeakSet();
let addStyle = true;
// this callback will be serialized as string and it never needs
// to be invoked multiple times. Each xworker here is bootstrapped
// only once thanks to the `sync.is_pyterminal()` check.
const workerReady = ({ interpreter, io, run }, { sync }) => {
if (!sync.is_pyterminal()) return;
// in workers it's always safe to grab the polyscript currentScript
run("from polyscript.currentScript import terminal as __terminal__");
// This part is inevitably duplicated as external scope
// can't be reached by workers out of the box.
// The detail is that here we use sync though, not readline.
const decoder = new TextDecoder();
let data = "";
const generic = {
isatty: true,
write(buffer) {
data = decoder.decode(buffer);
sync.pyterminal_write(data);
return buffer.length;
},
};
interpreter.setStdout(generic);
interpreter.setStderr(generic);
interpreter.setStdin({
isatty: true,
stdin: () => sync.pyterminal_read(data),
});
io.stderr = (error) => {
sync.pyterminal_write(`${error.message || error}\n`);
};
};
const pyTerminal = async () => { const pyTerminal = async () => {
const terminals = document.querySelectorAll(SELECTOR); const terminals = document.querySelectorAll(SELECTOR);
// no results will look further for runtime nodes const unknown = [].filter.call(terminals, notParsedYet);
if (!terminals.length) return;
// if we arrived this far, let's drop the MutationObserver // no results will look further for runtime nodes
// as we only support one terminal per page (right now). if (!unknown.length) return;
mo.disconnect(); // early flag elements as known to avoid concurrent
// MutationObserver invokes of this async handler
else unknown.forEach(bootstrapped.add, bootstrapped);
// we currently support only one terminal as in "classic" // we currently support only one terminal as in "classic"
if (terminals.length > 1) notifyAndThrow("You can use at most 1 terminal."); if ([].filter.call(terminals, onceOnMain).length > 1)
notifyAndThrow("You can use at most 1 main terminal");
const [element] = terminals;
// hopefully to be removed in the near future!
if (element.matches('script[type="mpy"],mpy-script'))
notifyAndThrow("Unsupported terminal.");
// import styles lazily // import styles lazily
if (addStyle) {
addStyle = false;
document.head.append( document.head.append(
Object.assign(document.createElement("link"), { Object.assign(document.createElement("link"), {
rel: "stylesheet", rel: "stylesheet",
href: new URL("./xterm.css", import.meta.url), href: new URL("./xterm.css", import.meta.url),
}), }),
); );
}
// lazy load these only when a valid terminal is found // lazy load these only when a valid terminal is found
const [{ Terminal }, { Readline }, { FitAddon }] = await Promise.all([ const [{ Terminal }, { Readline }, { FitAddon }] = await Promise.all([
@@ -46,6 +89,11 @@ const pyTerminal = async () => {
import(/* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js"), import(/* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js"),
]); ]);
for (const element of unknown) {
// hopefully to be removed in the near future!
if (element.matches('script[type="mpy"],mpy-script'))
notifyAndThrow("Unsupported terminal.");
const readline = new Readline(); const readline = new Readline();
// common main thread initialization for both worker // common main thread initialization for both worker
@@ -76,47 +124,32 @@ const pyTerminal = async () => {
terminal.open(target); terminal.open(target);
fitAddon.fit(); fitAddon.fit();
terminal.focus(); terminal.focus();
defineProperty(element, "terminal", { value: terminal });
return terminal;
}; };
// branch logic for the worker // branch logic for the worker
if (element.hasAttribute("worker")) { if (element.hasAttribute("worker")) {
// when the remote thread onReady triggers:
// setup the interpreter stdout and stderr
const workerReady = ({ interpreter }, { sync }) => {
sync.pyterminal_drop_hooks();
const decoder = new TextDecoder();
let data = "";
const generic = {
isatty: true,
write(buffer) {
data = decoder.decode(buffer);
sync.pyterminal_write(data);
return buffer.length;
},
};
interpreter.setStdout(generic);
interpreter.setStderr(generic);
interpreter.setStdin({
isatty: true,
stdin: () => sync.pyterminal_read(data),
});
};
// add a hook on the main thread to setup all sync helpers // add a hook on the main thread to setup all sync helpers
// also bootstrapping the XTerm target on main // also bootstrapping the XTerm target on main *BUT* ...
hooks.main.onWorker.add(function worker(_, xworker) { hooks.main.onWorker.add(function worker(_, xworker) {
// ... as multiple workers will add multiple callbacks
// be sure no xworker is ever initialized twice!
if (bootstrapped.has(xworker)) return;
bootstrapped.add(xworker);
// still cleanup this callback for future scripts/workers
hooks.main.onWorker.delete(worker); hooks.main.onWorker.delete(worker);
init({ init({
disableStdin: false, disableStdin: false,
cursorBlink: true, cursorBlink: true,
cursorStyle: "block", cursorStyle: "block",
}); });
xworker.sync.is_pyterminal = () => true;
xworker.sync.pyterminal_read = readline.read.bind(readline); xworker.sync.pyterminal_read = readline.read.bind(readline);
xworker.sync.pyterminal_write = readline.write.bind(readline); xworker.sync.pyterminal_write = readline.write.bind(readline);
// allow a worker to drop main thread hooks ASAP
xworker.sync.pyterminal_drop_hooks = () => {
hooks.worker.onReady.delete(workerReady);
};
}); });
// setup remote thread JS/Python code for whenever the // setup remote thread JS/Python code for whenever the
@@ -125,22 +158,45 @@ const pyTerminal = async () => {
} 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
hooks.main.onReady.add(function main({ io }) { hooks.main.onReady.add(function main({ interpreter, io, run }) {
console.warn("py-terminal is read only on main thread"); console.warn("py-terminal is read only on main thread");
hooks.main.onReady.delete(main); hooks.main.onReady.delete(main);
init({
// on main, it's easy to trash and clean the current terminal
globalThis.__py_terminal__ = init({
disableStdin: true, disableStdin: true,
cursorBlink: false, cursorBlink: false,
cursorStyle: "underline", cursorStyle: "underline",
}); });
io.stdout = (value) => { run("from js import __py_terminal__ as __terminal__");
readline.write(`${value}\n`); delete globalThis.__py_terminal__;
// This part is inevitably duplicated as external scope
// can't be reached by workers out of the box.
// The detail is that here we use readline here, not sync.
const decoder = new TextDecoder();
let data = "";
const generic = {
isatty: true,
write(buffer) {
data = decoder.decode(buffer);
readline.write(data);
return buffer.length;
},
}; };
interpreter.setStdout(generic);
interpreter.setStderr(generic);
interpreter.setStdin({
isatty: true,
stdin: () => readline.read(data),
});
io.stderr = (error) => { io.stderr = (error) => {
readline.write(`${error.message || error}\n`); readline.write(`${error.message || error}\n`);
}; };
}); });
} }
}
}; };
const mo = new MutationObserver(pyTerminal); const mo = new MutationObserver(pyTerminal);

View File

@@ -43,6 +43,8 @@ from pyscript.magic_js import (
try: try:
from pyscript.event_handling import when from pyscript.event_handling import when
except: 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 from pyscript.util import NotSupported
when = NotSupported( when = NotSupported(

View File

@@ -1,6 +1,14 @@
import inspect import inspect
try:
from pyodide.ffi.wrappers import add_event_listener 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 from pyscript.magic_js import document
@@ -27,7 +35,7 @@ def when(event_type=None, selector=None):
f"Invalid selector: {selector}. Selector must" f"Invalid selector: {selector}. Selector must"
" be a string, a pydom.Element or a pydom.ElementCollection." " be a string, a pydom.Element or a pydom.ElementCollection."
) )
try:
sig = inspect.signature(func) sig = inspect.signature(func)
# Function doesn't receive events # Function doesn't receive events
if not sig.parameters: if not sig.parameters:
@@ -35,11 +43,24 @@ def when(event_type=None, selector=None):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
func() func()
else:
wrapper = func
except AttributeError:
# TODO: this is currently an quick hack to get micropython working but we need
# to actually properly replace inspect.signature with something else
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except TypeError as e:
if "takes 0 positional arguments" in str(e):
return func()
raise
for el in elements: for el in elements:
add_event_listener(el, event_type, wrapper) add_event_listener(el, event_type, wrapper)
else:
for el in elements:
add_event_listener(el, event_type, func)
return func return func
return decorator return decorator

View File

@@ -1,9 +1,28 @@
import sys
import js as globalThis import js as globalThis
from polyscript import js_modules from polyscript import js_modules
from pyscript.util import NotSupported from pyscript.util import NotSupported
RUNNING_IN_WORKER = not hasattr(globalThis, "document") RUNNING_IN_WORKER = not hasattr(globalThis, "document")
# allow `from pyscript.js_modules.xxx import yyy`
class JSModule:
def __init__(self, name):
self.name = name
def __getattr__(self, field):
# avoid pyodide looking for non existent fields
if not field.startswith("_"):
return getattr(getattr(js_modules, self.name), field)
# generate N modules in the system that will proxy the real value
for name in globalThis.Reflect.ownKeys(js_modules):
sys.modules[f"pyscript.js_modules.{name}"] = JSModule(name)
sys.modules["pyscript.js_modules"] = js_modules
if RUNNING_IN_WORKER: if RUNNING_IN_WORKER:
import js import js
import polyscript import polyscript

View File

@@ -0,0 +1 @@
from .pydom import dom as pydom

View File

@@ -0,0 +1,95 @@
from pyodide.ffi import to_js
from pyscript import window
class Device:
"""Device represents a media input or output device, such as a microphone,
camera, or headset.
"""
def __init__(self, device):
self._js = device
@property
def id(self):
return self._js.deviceId
@property
def group(self):
return self._js.groupId
@property
def kind(self):
return self._js.kind
@property
def label(self):
return self._js.label
def __getitem__(self, key):
return getattr(self, key)
@classmethod
async def load(cls, audio=False, video=True):
"""Load the device stream."""
options = window.Object.new()
options.audio = audio
if isinstance(video, bool):
options.video = video
else:
# 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:
setattr(
options.video,
k,
to_js(video[k], dict_converter=window.Object.fromEntries),
)
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)
async def list_devices() -> list[dict]:
"""
Return the list of the currently available media input and output devices,
such as microphones, cameras, headsets, and so forth.
Output:
list(dict) - list of dictionaries representing the available media devices.
Each dictionary has the following keys:
* deviceId: a string that is an identifier for the represented device
that is persisted across sessions. It is un-guessable by other
applications and unique to the origin of the calling application.
It is reset when the user clears cookies (for Private Browsing, a
different identifier is used that is not persisted across sessions).
* groupId: a string that is a group identifier. Two devices have the same
group identifier if they belong to the same physical device — for
example a monitor with both a built-in camera and a microphone.
* kind: an enumerated value that is either "videoinput", "audioinput"
or "audiooutput".
* label: a string describing this device (for example "External USB
Webcam").
Note: the returned list will omit any devices that are blocked by the document
Permission Policy: microphone, camera, speaker-selection (for output devices),
and so on. Access to particular non-default devices is also gated by the
Permissions API, and the list will omit devices for which the user has not
granted explicit permission.
"""
# https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
return [
Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices()
]

View File

@@ -1,9 +1,34 @@
import sys try:
import warnings
from functools import cached_property
from typing import Any from typing import Any
except ImportError:
Any = "Any"
try:
import warnings
except ImportError:
# TODO: For now it probably means we are in MicroPython. We should figure
# out the "right" way to handle this. For now we just ignore the warning
# and logging to console
class warnings:
@staticmethod
def warn(*args, **kwargs):
print("WARNING: ", *args, **kwargs)
try:
from functools import cached_property
except ImportError:
# TODO: same comment about micropython as above
cached_property = property
try:
from pyodide.ffi import JsProxy from pyodide.ffi import JsProxy
except ImportError:
# TODO: same comment about micropython as above
def JsProxy(obj):
return obj
from pyscript import display, document, window from pyscript import display, document, window
alert = window.alert alert = window.alert
@@ -204,6 +229,91 @@ class Element(BaseElement):
def show_me(self): def show_me(self):
self._js.scrollIntoView() self._js.scrollIntoView()
def snap(
self,
to: BaseElement | str = None,
width: int | None = None,
height: int | None = None,
):
"""
Captures a snapshot of a video element. (Only available for video elements)
Inputs:
* to: element where to save the snapshot of the video frame to
* width: width of the image
* height: height of the image
Output:
(Element) canvas element where the video frame snapshot was drawn into
"""
if self._js.tagName != "VIDEO":
raise AttributeError("Snap method is only available for video Elements")
if to is None:
canvas = self.create("canvas")
if width is None:
width = self._js.width
if height is None:
height = self._js.height
canvas._js.width = width
canvas._js.height = height
elif isistance(to, Element):
if to._js.tagName != "CANVAS":
raise TypeError("Element to snap to must a canvas.")
canvas = to
elif getattr(to, "tagName", "") == "CANVAS":
canvas = Element(to)
elif isinstance(to, str):
canvas = pydom[to][0]
if canvas._js.tagName != "CANVAS":
raise TypeError("Element to snap to must a be canvas.")
canvas.draw(self, width, height)
return canvas
def download(self, filename: str = "snapped.png") -> None:
"""Download the current element (only available for canvas elements) with the filename
provided in input.
Inputs:
* filename (str): name of the file being downloaded
Output:
None
"""
if self._js.tagName != "CANVAS":
raise AttributeError(
"The download method is only available for canvas Elements"
)
link = self.create("a")
link._js.download = filename
link._js.href = self._js.toDataURL()
link._js.click()
def draw(self, what, width, height):
"""Draw `what` on the current element (only available for canvas elements).
Inputs:
* what (canvas image source): An element to draw into the context. The specification permits any canvas
image source, specifically, an HTMLImageElement, an SVGImageElement, an HTMLVideoElement,
an HTMLCanvasElement, an ImageBitmap, an OffscreenCanvas, or a VideoFrame.
"""
if self._js.tagName != "CANVAS":
raise AttributeError(
"The draw method is only available for canvas Elements"
)
if isinstance(what, Element):
what = what._js
# https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
self._js.getContext("2d").drawImage(what, 0, 0, width, height)
class OptionsProxy: class OptionsProxy:
"""This class represents the options of a select element. It """This class represents the options of a select element. It
@@ -276,7 +386,7 @@ class OptionsProxy:
return self.options[key] return self.options[key]
class StyleProxy(dict): class StyleProxy: # (dict):
def __init__(self, element: Element) -> None: def __init__(self, element: Element) -> None:
self._element = element self._element = element
@@ -395,7 +505,7 @@ class ElementCollection:
class DomScope: class DomScope:
def __getattr__(self, __name: str) -> Any: def __getattr__(self, __name: str):
element = document[f"#{__name}"] element = document[f"#{__name}"]
if element: if element:
return element[0] return element[0]
@@ -409,7 +519,12 @@ class PyDom(BaseElement):
ElementCollection = ElementCollection ElementCollection = ElementCollection
def __init__(self): def __init__(self):
super().__init__(document) # PyDom is a special case of BaseElement where we don't want to create a new JS element
# and it really doesn't have a need for styleproxy or parent to to call to __init__
# (which actually fails in MP for some reason)
self._js = document
self._parent = None
self._proxies = {}
self.ids = DomScope() self.ids = DomScope()
self.body = Element(document.body) self.body = Element(document.body)
self.head = Element(document.head) self.head = Element(document.head)
@@ -418,10 +533,6 @@ class PyDom(BaseElement):
return super().create(type_, is_child=False, classes=classes, html=html) return super().create(type_, is_child=False, classes=classes, html=html)
def __getitem__(self, key): def __getitem__(self, key):
if isinstance(key, int):
indices = range(*key.indices(len(self.list)))
return [self.list[i] for i in indices]
elements = self._js.querySelectorAll(key) elements = self._js.querySelectorAll(key)
if not elements: if not elements:
return None return None
@@ -429,5 +540,3 @@ class PyDom(BaseElement):
dom = PyDom() dom = PyDom()
sys.modules[__name__] = dom

View File

@@ -1,4 +1,7 @@
export default { export default {
// allow pyterminal checks to bootstrap
is_pyterminal: () => false,
/** /**
* 'Sleep' for the given number of seconds. Used to implement Python's time.sleep in Worker threads. * 'Sleep' for the given number of seconds. Used to implement Python's time.sleep in Worker threads.
* @param {number} seconds The number of seconds to sleep. * @param {number} seconds The number of seconds to sleep.

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Media Example</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<script type="py" src="camera.py" async></script>
<label for="cars">Choose a device:</label>
<select name="devices" id="devices"></select>
<button id="pick-device">Select the device</button>
<button id="snap">Snap</button>
<div id="result"></div>
<video id="video" width="600" height="400" autoplay></video>
</body>
</html>

View File

@@ -0,0 +1,32 @@
from pyodide.ffi import create_proxy
from pyscript import display, document, when, window
from pyweb import media, pydom
devicesSelect = pydom["#devices"][0]
video = pydom["video"][0]
devices = {}
async def list_media_devices(event=None):
"""List the available media devices."""
global devices
for i, device in enumerate(await media.list_devices()):
devices[device.id] = device
label = f"{i} - ({device.kind}) {device.label} [{device.id}]"
devicesSelect.options.add(value=device.id, html=label)
@when("click", "#pick-device")
async def connect_to_device(e):
"""Connect to the selected device."""
device = devices[devicesSelect.value]
video._js.srcObject = await device.get_stream()
@when("click", "#snap")
async def camera_click(e):
"""Take a picture and download it."""
video.snap().download()
await list_media_devices()

View File

@@ -6,6 +6,12 @@
<title>PyScript Next Plugin</title> <title>PyScript Next Plugin</title>
<link rel="stylesheet" href="../dist/core.css"> <link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script> <script type="module" src="../dist/core.js"></script>
<py-config src="bad.toml" type="toml"></py-config> <mpy-config src="config-url/config.json"></mpy-config>
<script type="mpy">
from runtime import test
</script>
<script type="mpy" worker>
from runtime import test
</script>
</head> </head>
</html> </html>

View File

@@ -0,0 +1,7 @@
{
"files":{
"{FROM}": "./src",
"{TO}": "./runtime",
"{FROM}/test.py": "{TO}/test.py"
}
}

View File

@@ -0,0 +1,8 @@
from pyscript import RUNNING_IN_WORKER, document
classList = document.documentElement.classList
if RUNNING_IN_WORKER:
classList.add("worker")
else:
classList.add("main")

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">
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<mpy-config>
[js_modules.main]
"./js_modules.js" = "random_js"
</mpy-config>
<mpy-script>
from pyscript.js_modules.random_js import default as value
from pyscript.js_modules import random_js
from pyscript import js_modules
print("mpy", value)
print("mpy", random_js.default)
print("mpy", js_modules.random_js.default)
</mpy-script>
<py-config>
[js_modules.main]
"./js_modules.js" = "random_js"
</py-config>
<py-script>
from pyscript.js_modules.random_js import default as value
from pyscript.js_modules import random_js
from pyscript import js_modules, document
print("py", value)
print("py", random_js.default)
print("py", js_modules.random_js.default)
document.documentElement.classList.add('done')
</py-script>
</body>
</html>

View File

@@ -0,0 +1 @@
export default Math.random();

View File

@@ -35,3 +35,46 @@ test('MicroPython hooks', async ({ page }) => {
'worker onAfterRun', 'worker onAfterRun',
].join('\n')); ].join('\n'));
}); });
test('MicroPython + Pyodide js_modules', async ({ page }) => {
const logs = [];
page.on('console', msg => {
const text = msg.text();
if (!text.startsWith('['))
logs.push(text);
});
await page.goto('http://localhost:8080/test/js_modules.html');
await page.waitForSelector('html.done');
await expect(logs.length).toBe(6);
await expect(logs[0]).toBe(logs[1]);
await expect(logs[1]).toBe(logs[2]);
await expect(logs[3]).toBe(logs[4]);
await expect(logs[4]).toBe(logs[5]);
});
test('MicroPython + configURL', async ({ page }) => {
const logs = [];
page.on('console', msg => {
const text = msg.text();
if (!text.startsWith('['))
logs.push(text);
});
await page.goto('http://localhost:8080/test/config-url.html');
await page.waitForSelector('html.main.worker');
});
test('Pyodide + terminal on Main', async ({ page }) => {
await page.goto('http://localhost:8080/test/py-terminal-main.html');
await page.waitForSelector('html.ok');
});
test('Pyodide + terminal on Worker', async ({ page }) => {
await page.goto('http://localhost:8080/test/py-terminal-worker.html');
await page.waitForSelector('html.ok');
});
test('Pyodide + multiple terminals via Worker', async ({ page }) => {
await page.goto('http://localhost:8080/test/py-terminals.html');
await page.waitForSelector('html.first.second');
});

View File

@@ -0,0 +1,20 @@
<!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="mpy">
from pyscript import document
import sys
document.body.append(sys.version)
</script>
<script type="py">
from pyscript import document
import sys
document.body.append(sys.version)
</script>
</body>
</html>

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PyScript Test</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<script type="py-editor">
0
</script>
<script type="py-editor">
1
</script>
<script type="py-editor">
2
</script>
<script type="py-editor">
3
</script>
<script type="py-editor">
4
</script>
<script type="py-editor">
5
</script>
<!-- more... -->
</body>
</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" />
<title>PyTerminal Main</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
<style>.xterm { padding: .5rem; }</style>
</head>
<body>
<py-script src="terminal.py" terminal></py-script>
</body>
</html>

View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyTerminal Main</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
<style>.xterm { padding: .5rem; }</style>
</head>
<body>
<script type="py" src="terminal.py" worker terminal></script>
<script type="py" src="terminal.py" worker terminal></script>
</body>
</html>

View File

@@ -14,6 +14,9 @@
print('hello world') print('hello world')
</script> </script>
<py-script worker terminal> <py-script worker terminal>
# works on both worker and main scripts
print("__terminal__", __terminal__)
import sys import sys
from pyscript import display, document from pyscript import display, document
display("Hello", "PyScript Next - PyTerminal", append=False) display("Hello", "PyScript Next - PyTerminal", append=False)

View File

@@ -0,0 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyTerminal Main</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
<style>.xterm { padding: .5rem; }</style>
</head>
<body>
<script type="py" worker terminal>
from pyscript import document
document.documentElement.classList.add("first")
import code
code.interact()
</script>
<script type="py" worker terminal>
from pyscript import document
document.documentElement.classList.add("second")
import code
code.interact()
</script>
</body>
</html>

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Next Plugin</title> <title>PyDom Example</title>
<link rel="stylesheet" href="../dist/core.css"> <link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script> <script type="module" src="../dist/core.js"></script>
</head> </head>

View File

@@ -1,26 +1,32 @@
import random import random
import time
from datetime import datetime as dt from datetime import datetime as dt
from pyscript import display from pyscript import display, when
from pyweb import pydom from pyweb import pydom
from pyweb.base import when
@when("click", "#just-a-button") @when("click", "#just-a-button")
def on_click(event): def on_click():
print(f"Hello from Python! {dt.now()}") try:
display(f"Hello from Python! {dt.now()}", append=False, target="result") timenow = dt.now()
except NotImplementedError:
# In this case we assume it's not implemented because we are using MycroPython
tnow = time.localtime()
tstr = "{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}"
timenow = tstr.format(tnow[2], tnow[1], tnow[0], *tnow[2:])
display(f"Hello from PyScript, time is: {timenow}", append=False, target="result")
@when("click", "#color-button") @when("click", "#color-button")
def on_color_click(event): def on_color_click(event):
print("1")
btn = pydom["#result"] btn = pydom["#result"]
print("2")
btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}" btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}"
def reset_color(): @when("click", "#color-reset-button")
def reset_color(*args, **kwargs):
pydom["#result"].style["background-color"] = "white" pydom["#result"].style["background-color"] = "white"

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyDom Example (MicroPython)</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<script type="mpy" src="pydom.py"></script>
<button id="just-a-button">Click For Time</button>
<button id="color-button">Click For Color</button>
<button id="color-reset-button">Reset Color</button>
<div id="result"></div>
</body>
</html>

View File

@@ -1,6 +1,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>PyperCard PyTest Suite</title> <title>PyDom Test Suite</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../../dist/core.css"> <link rel="stylesheet" href="../../dist/core.css">
@@ -32,7 +32,7 @@
</style> </style>
</head> </head>
<body> <body>
<script type="py" src="run_tests.py" config="tests.toml"></script> <script type="py" src="./run_tests.py" config="./tests.toml"></script>
<h1>pyscript.dom Tests</h1> <h1>pyscript.dom Tests</h1>
<p>You can pass test parameters to this test suite by passing them as query params on the url. <p>You can pass test parameters to this test suite by passing them as query params on the url.

View File

@@ -336,7 +336,7 @@ class TestSelect:
assert select.options[0].html == "Option 1" assert select.options[0].html == "Option 1"
# WHEN we add another option (blank this time) # WHEN we add another option (blank this time)
select.options.add() select.options.add("")
# EXPECT the select element to have 2 options # EXPECT the select element to have 2 options
assert len(select.options) == 2 assert len(select.options) == 2

View File

@@ -0,0 +1,8 @@
from pyscript import document
classList = document.documentElement.classList
if not __terminal__:
classList.add("error")
else:
classList.add("ok")

View File

@@ -17,6 +17,7 @@ from playwright.sync_api import Error as PlaywrightError
ROOT = py.path.local(__file__).dirpath("..", "..", "..") ROOT = py.path.local(__file__).dirpath("..", "..", "..")
BUILD = ROOT.join("pyscript.core").join("dist") BUILD = ROOT.join("pyscript.core").join("dist")
TEST = ROOT.join("pyscript.core").join("test")
def params_with_marks(params): def params_with_marks(params):
@@ -206,6 +207,14 @@ class PyScriptTest:
self.tmpdir = tmpdir self.tmpdir = tmpdir
# create a symlink to BUILD inside tmpdir # create a symlink to BUILD inside tmpdir
tmpdir.join("build").mksymlinkto(BUILD) tmpdir.join("build").mksymlinkto(BUILD)
# create a symlink ALSO to dist folder so we can run the tests in
# the test folder
tmpdir.join("dist").mksymlinkto(BUILD)
# create a symlink to TEST inside tmpdir so we can run tests in that
# manual test folder
tmpdir.join("test").mksymlinkto(TEST)
# create a symlink to the favicon, so that we can use it in the HTML
self.tmpdir.chdir() self.tmpdir.chdir()
self.tmpdir.join("favicon.ico").write("") self.tmpdir.join("favicon.ico").write("")
self.logger = logger self.logger = logger

View File

@@ -0,0 +1,30 @@
from .support import PyScriptTest, with_execution_thread
@with_execution_thread(None)
class TestSmokeTests(PyScriptTest):
"""
Each example requires the same three tests:
- Test that the initial markup loads properly (currently done by
testing the <title> tag's content)
- Testing that pyscript is loading properly
- Testing that the page contains appropriate content after rendering
"""
def test_pydom(self):
# Test the full pydom test suite by running it in the browser
self.goto("test/pyscript_dom/index.html?-v&-s")
assert self.page.title() == "PyDom Test Suite"
# wait for the test suite to finish
self.wait_for_console(
"============================= test session starts =============================="
)
self.assert_no_banners()
results = self.page.inner_html("#tests-terminal")
assert results
assert "PASSED" in results
assert "FAILED" not in results

View File

@@ -7,6 +7,7 @@ from .support import PageErrors, PyScriptTest, only_worker, skip_worker
class TestPyTerminal(PyScriptTest): class TestPyTerminal(PyScriptTest):
@skip_worker("We do support multiple worker terminal now")
def test_multiple_terminals(self): def test_multiple_terminals(self):
""" """
Multiple terminals are not currently supported Multiple terminals are not currently supported
@@ -19,9 +20,9 @@ class TestPyTerminal(PyScriptTest):
wait_for_pyscript=False, wait_for_pyscript=False,
check_js_errors=False, check_js_errors=False,
) )
assert self.assert_banner_message("You can use at most 1 terminal") assert self.assert_banner_message("You can use at most 1 main terminal")
with pytest.raises(PageErrors, match="You can use at most 1 terminal"): with pytest.raises(PageErrors, match="You can use at most 1 main terminal"):
self.check_js_errors() self.check_js_errors()
# TODO: interactive shell still unclear # TODO: interactive shell still unclear
@@ -112,7 +113,7 @@ class TestPyTerminal(PyScriptTest):
This test isn't meant to capture all of the behaviors of an xtermjs terminal; This test isn't meant to capture all of the behaviors of an xtermjs terminal;
rather, it confirms with a few basic formatting sequences that (1) the xtermjs rather, it confirms with a few basic formatting sequences that (1) the xtermjs
terminal is functioning/loaded correctly and (2) that output toward that terminal terminal is functioning/loaded correctly and (2) that output toward that terminal
isn't being escaped in a way that prevents it reacting to escape seqeunces. The isn't being escaped in a way that prevents it reacting to escape sequences. The
main goal is preventing regressions. main goal is preventing regressions.
""" """
self.pyscript_run( self.pyscript_run(

View File

@@ -7,6 +7,8 @@ declare namespace _default {
"util.py": string; "util.py": string;
}; };
let pyweb: { let pyweb: {
"__init__.py": string;
"media.py": string;
"pydom.py": string; "pydom.py": string;
}; };
} }

View File

@@ -1,4 +1,5 @@
declare namespace _default { declare namespace _default {
function is_pyterminal(): boolean;
/** /**
* 'Sleep' for the given number of seconds. Used to implement Python's time.sleep in Worker threads. * 'Sleep' for the given number of seconds. Used to implement Python's time.sleep in Worker threads.
* @param {number} seconds The number of seconds to sleep. * @param {number} seconds The number of seconds to sleep.

View File

@@ -1,13 +1,13 @@
black black==23.11.0
isort isort==5.12.0
pytest==7.1.2 pytest==7.1.2
pre-commit pre-commit==3.5.0
playwright==1.33.0 playwright==1.33.0
pytest-playwright==0.3.3 pytest-playwright==0.3.3
pytest-xdist==3.3.0 pytest-xdist==3.3.0
pexpect pexpect==4.9.0
pyodide_py==0.24.1 pyodide_py==0.24.1
micropip micropip==0.5.0
toml toml==0.10.2
numpy numpy==1.26.2
pillow pillow==10.1.0