mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 02:37:41 -05:00
Yet another refactoring to untangle the old mess. Highlights: base.ts, pyscript.ts and pyrepl.ts were a tangled mess of code, in which each of them interacted with the others in non-obvious ways. Now PyScript is no longer a subclass of BaseEvalElement and it is much simpler. I removed code for handling the attributes std-out and std-err because they are no longer needed with the new display() logic. The logic for executing python code is now in pyexec.ts: so we are decoupling the process of "finding" the python code (handled by the py-script web component) and the logic to actually execute it. This has many advantages, including the fact that it will be more easily usable by other components (e.g. pyrepl). Also, note that it's called pyexec and not pyeval: in the vast majority of cases in Python you have statements to execute, and almost never expressions to evaluate. I killed the last remaining global store, scriptQueue tada. As a bonus effect, now we automatically do the correct thing when a <py-script> tag is dynamically added to the DOM (I added a test for it). I did not remove svelte from packages.json, because I don't fully understand the implications: there are various options which mention svelte in rollup.js and tsconfig.json, so it's probably better to kill it in its own PR. pyexec.ts is also responsible of handling the default target for display() and correct handling/visualization of exceptions. I fixed/improved/added display/output tests in the process. I also found a problem though, see issue #878, so I improved the test and marked it as xfail. I removed BaseEvalElement as the superclass of most components. Now the only class which inherits from it is PyRepl. In a follow-up PR, I plan to merge them into a single class and do more cleanup. During the refactoring, I killed guidGenerator: now instead of generating random py-* IDs which are very hard to read for humans, we generated py-internal-X IDs, where X is 0, 1, 2, 3, etc. This makes writing tests and debugging much easier. I improved a lot our test machinery: it turns out that PR #829 broke the ability to use/view sourcemaps inside the playwright browser (at least on my machine). For some reason chromium is unable to find sourcemaps if you use playwrights internal routing. So I reintroduced the http_server fixture which was removed by that PR, and added a pytest option --no-fake-server to use it instead, useful for debugging. By default we are still using the fakeserver though (which is faster and parallelizable). Similarly, I added --dev which implies --headed and also automatically open chrome dev tools.
192 lines
6.7 KiB
TypeScript
192 lines
6.7 KiB
TypeScript
import { htmlDecode, ensureUniqueId } from '../utils';
|
|
import type { Runtime } from '../runtime';
|
|
import { getLogger } from '../logger';
|
|
import { pyExec } from '../pyexec';
|
|
|
|
const logger = getLogger('py-script');
|
|
|
|
export function make_PyScript(runtime: Runtime) {
|
|
|
|
class PyScript extends HTMLElement {
|
|
|
|
async connectedCallback() {
|
|
ensureUniqueId(this);
|
|
const pySrc = await this.getPySrc();
|
|
this.innerHTML = '';
|
|
await pyExec(runtime, pySrc, this);
|
|
}
|
|
|
|
async getPySrc(): Promise<string> {
|
|
if (this.hasAttribute('src')) {
|
|
// XXX: what happens if the fetch() fails?
|
|
// We should handle the case correctly, but in my defense
|
|
// this case was broken also before the refactoring. FIXME!
|
|
const url = this.getAttribute('src');
|
|
const response = await fetch(url);
|
|
return await response.text();
|
|
}
|
|
else {
|
|
return htmlDecode(this.innerHTML);
|
|
}
|
|
}
|
|
}
|
|
|
|
return PyScript;
|
|
}
|
|
|
|
/** Defines all possible py-on* and their corresponding event types */
|
|
const pyAttributeToEvent: Map<string, string> = new Map<string, string>([
|
|
// Leaving pys-onClick and pys-onKeyDown for backward compatibility
|
|
["pys-onClick", "click"],
|
|
["pys-onKeyDown", "keydown"],
|
|
["py-onClick", "click"],
|
|
["py-onKeyDown", "keydown"],
|
|
// Window Events
|
|
["py-afterprint", "afterprint"],
|
|
["py-beforeprint", "beforeprint"],
|
|
["py-beforeunload", "beforeunload"],
|
|
["py-error", "error"],
|
|
["py-hashchange", "hashchange"],
|
|
["py-load", "load"],
|
|
["py-message", "message"],
|
|
["py-offline", "offline"],
|
|
["py-online", "online"],
|
|
["py-pagehide", "pagehide"],
|
|
["py-pageshow", "pageshow"],
|
|
["py-popstate", "popstate"],
|
|
["py-resize", "resize"],
|
|
["py-storage", "storage"],
|
|
["py-unload", "unload"],
|
|
|
|
// Form Events
|
|
["py-blur", "blur"],
|
|
["py-change", "change"],
|
|
["py-contextmenu", "contextmenu"],
|
|
["py-focus", "focus"],
|
|
["py-input", "input"],
|
|
["py-invalid", "invalid"],
|
|
["py-reset", "reset"],
|
|
["py-search", "search"],
|
|
["py-select", "select"],
|
|
["py-submit", "submit"],
|
|
|
|
// Keyboard Events
|
|
["py-keydown", "keydown"],
|
|
["py-keypress", "keypress"],
|
|
["py-keyup", "keyup"],
|
|
|
|
// Mouse Events
|
|
["py-click", "click"],
|
|
["py-dblclick", "dblclick"],
|
|
["py-mousedown", "mousedown"],
|
|
["py-mousemove", "mousemove"],
|
|
["py-mouseout", "mouseout"],
|
|
["py-mouseover", "mouseover"],
|
|
["py-mouseup", "mouseup"],
|
|
["py-mousewheel", "mousewheel"],
|
|
["py-wheel", "wheel"],
|
|
|
|
// Drag Events
|
|
["py-drag", "drag"],
|
|
["py-dragend", "dragend"],
|
|
["py-dragenter", "dragenter"],
|
|
["py-dragleave", "dragleave"],
|
|
["py-dragover", "dragover"],
|
|
["py-dragstart", "dragstart"],
|
|
["py-drop", "drop"],
|
|
["py-scroll", "scroll"],
|
|
|
|
// Clipboard Events
|
|
["py-copy", "copy"],
|
|
["py-cut", "cut"],
|
|
["py-paste", "paste"],
|
|
|
|
// Media Events
|
|
["py-abort", "abort"],
|
|
["py-canplay", "canplay"],
|
|
["py-canplaythrough", "canplaythrough"],
|
|
["py-cuechange", "cuechange"],
|
|
["py-durationchange", "durationchange"],
|
|
["py-emptied", "emptied"],
|
|
["py-ended", "ended"],
|
|
["py-loadeddata", "loadeddata"],
|
|
["py-loadedmetadata", "loadedmetadata"],
|
|
["py-loadstart", "loadstart"],
|
|
["py-pause", "pause"],
|
|
["py-play", "play"],
|
|
["py-playing", "playing"],
|
|
["py-progress", "progress"],
|
|
["py-ratechange", "ratechange"],
|
|
["py-seeked", "seeked"],
|
|
["py-seeking", "seeking"],
|
|
["py-stalled", "stalled"],
|
|
["py-suspend", "suspend"],
|
|
["py-timeupdate", "timeupdate"],
|
|
["py-volumechange", "volumechange"],
|
|
["py-waiting", "waiting"],
|
|
|
|
// Misc Events
|
|
["py-toggle", "toggle"],
|
|
]);
|
|
|
|
/** Initialize all elements with py-* handlers attributes */
|
|
export async function initHandlers(runtime: Runtime) {
|
|
logger.debug('Initializing py-* event handlers...');
|
|
for (const pyAttribute of pyAttributeToEvent.keys()) {
|
|
await createElementsWithEventListeners(runtime, pyAttribute);
|
|
}
|
|
}
|
|
|
|
/** Initializes an element with the given py-on* attribute and its handler */
|
|
async function createElementsWithEventListeners(runtime: Runtime, pyAttribute: string): Promise<void> {
|
|
const matches: NodeListOf<HTMLElement> = document.querySelectorAll(`[${pyAttribute}]`);
|
|
for (const el of matches) {
|
|
if (el.id.length === 0) {
|
|
throw new TypeError(`<${el.tagName.toLowerCase()}> must have an id attribute, when using the ${pyAttribute} attribute`)
|
|
}
|
|
const handlerCode = el.getAttribute(pyAttribute);
|
|
const event = pyAttributeToEvent.get(pyAttribute);
|
|
|
|
if (pyAttribute === 'pys-onClick' || pyAttribute === 'pys-onKeyDown'){
|
|
console.warn("Use of pys-onClick and pys-onKeyDown attributes is deprecated in favor of py-onClick() and py-onKeyDown(). pys-on* attributes will be deprecated in a future version of PyScript.")
|
|
const source = `
|
|
from pyodide.ffi import create_proxy
|
|
Element("${el.id}").element.addEventListener("${event}", create_proxy(${handlerCode}))
|
|
`;
|
|
await runtime.run(source);
|
|
}
|
|
else{
|
|
el.addEventListener(event, () => {
|
|
(async() => {await runtime.run(handlerCode)})();
|
|
});
|
|
}
|
|
// TODO: Should we actually map handlers in JS instead of Python?
|
|
// el.onclick = (evt: any) => {
|
|
// console.log("click");
|
|
// new Promise((resolve, reject) => {
|
|
// setTimeout(() => {
|
|
// console.log('Inside')
|
|
// }, 300);
|
|
// }).then(() => {
|
|
// console.log("resolved")
|
|
// });
|
|
// // let handlerCode = el.getAttribute('py-onClick');
|
|
// // pyodide.runPython(handlerCode);
|
|
// }
|
|
}
|
|
|
|
}
|
|
|
|
/** Mount all elements with attribute py-mount into the Python namespace */
|
|
export async function mountElements(runtime: Runtime) {
|
|
const matches: NodeListOf<HTMLElement> = document.querySelectorAll('[py-mount]');
|
|
logger.info(`py-mount: found ${matches.length} elements`);
|
|
|
|
let source = '';
|
|
for (const el of matches) {
|
|
const mountName = el.getAttribute('py-mount') || el.id.split('-').join('_');
|
|
source += `\n${mountName} = Element("${el.id}")`;
|
|
}
|
|
await runtime.run(source);
|
|
}
|