Compare commits

...

6 Commits

Author SHA1 Message Date
Andrea Giammarchi
4b90ebdef5 Bring back pyweb as it was (#2105) 2024-06-21 14:49:20 +02:00
Andrea Giammarchi
15c19aa708 Updated Polyscript with latest MicroPython (#2103) 2024-06-19 17:56:22 +02:00
Andrea Giammarchi
d0406be84c A persistent IndexedDB store for PyScript (#2101)
A persistent IndexedDB store for PyScript
2024-06-19 14:11:57 +02:00
Andrea Giammarchi
aab015b9b8 Better py editor indentation (#2098)
Better PyEditor Indentation
2024-06-13 11:34:14 +02:00
Andrea Giammarchi
a1e5a05b49 PyEditor cumulative fixes & improvements (#2095)
* PyEditor fixes

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

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

* PyEditor cumulative fixes & improvements

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-12 18:55:36 +02:00
Fabio Pliger
f1a787e031 move pydom and elements from pyweb to pyscript.web (#2092)
* change pydom example to use new pyscript.web namespace

* change tests to use new pyscript.web namespace

* create new pyscript.web package and move pydom to pyscript.web.dom

* add __init__ to pyscript.web and expose the dom instance instead of the pyscript.web.dom module

* move elements from pyweb.ui to pyscript.web and temp fix pydom import

* moved of elements file completed

* moved media from pyweb to pyscript.web

* RIP pyweb

* move JSProperty from pyscript.web.dom to pyscript.web.elements

* move element classes from pyscript.web.dom to pyscript.web.elements

* first round of fixes while running tests

* fix test typo

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

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

* restore right type type returned for Element.parent. ALL TESTS PASS LOCALLY NOW

* lint

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

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

* clean up dom.py from dead commented code and osbolete comments

* bugfix: dom shouldn't return None when it can't find any element for a specific selector so it now returns an empty collection

* additional cleanup in tests

* lint

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-06 15:42:14 +02:00
16 changed files with 2428 additions and 189 deletions

View File

@@ -38,11 +38,11 @@ To try PyScript, import the appropriate pyscript files into the `<head>` tag of
<head> <head>
<link <link
rel="stylesheet" rel="stylesheet"
href="https://pyscript.net/releases/2024.5.2/core.css" href="https://pyscript.net/releases/2024.6.1/core.css"
/> />
<script <script
type="module" type="module"
src="https://pyscript.net/releases/2024.5.2/core.js" src="https://pyscript.net/releases/2024.6.1/core.js"
></script> ></script>
</head> </head>
<body> <body>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.4.42", "version": "0.4.50",
"type": "module", "type": "module",
"description": "PyScript", "description": "PyScript",
"module": "./index.js", "module": "./index.js",
@@ -20,8 +20,9 @@
}, },
"scripts": { "scripts": {
"server": "npx static-handler --coi .", "server": "npx static-handler --coi .",
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy", "build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run ts && npm run test:mpy; fi",
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/", "build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
"build:flatted": "node rollup/flatted.cjs",
"build:plugins": "node rollup/plugins.cjs", "build:plugins": "node rollup/plugins.cjs",
"build:stdlib": "node rollup/stdlib.cjs", "build:stdlib": "node rollup/stdlib.cjs",
"build:3rd-party": "node rollup/3rd-party.cjs", "build:3rd-party": "node rollup/3rd-party.cjs",
@@ -43,7 +44,7 @@
"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.12.14", "polyscript": "^0.13.5",
"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"
@@ -53,18 +54,19 @@
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
"@codemirror/language": "^6.10.2", "@codemirror/language": "^6.10.2",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.27.0", "@codemirror/view": "^6.28.1",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@rollup/plugin-commonjs": "^25.0.8", "@rollup/plugin-commonjs": "^26.0.1",
"@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",
"@webreflection/toml-j0.4": "^1.1.3", "@webreflection/toml-j0.4": "^1.1.3",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
"bun": "^1.1.12", "bun": "^1.1.14",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"eslint": "^9.4.0", "eslint": "^9.5.0",
"flatted": "^3.3.1",
"rollup": "^4.18.0", "rollup": "^4.18.0",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",

View File

@@ -0,0 +1,17 @@
const { writeFileSync, readFileSync } = require("node:fs");
const { join } = require("node:path");
const flatted = "# https://www.npmjs.com/package/flatted\n\n";
const source = join(
__dirname,
"..",
"node_modules",
"flatted",
"python",
"flatted.py",
);
const dest = join(__dirname, "..", "src", "stdlib", "pyscript", "flatted.py");
const clear = (str) => String(str).replace(/^#.*/gm, "").trimStart();
writeFileSync(dest, flatted + clear(readFileSync(source)));

View File

@@ -86,21 +86,24 @@ async function execute({ currentTarget }) {
}); });
} }
const makeRunButton = (listener, type) => { const makeRunButton = (handler, type) => {
const runButton = document.createElement("button"); const runButton = document.createElement("button");
runButton.className = `absolute ${type}-editor-run-button`; runButton.className = `absolute ${type}-editor-run-button`;
runButton.innerHTML = RUN_BUTTON; runButton.innerHTML = RUN_BUTTON;
runButton.setAttribute("aria-label", "Python Script Run Button"); runButton.setAttribute("aria-label", "Python Script Run Button");
runButton.addEventListener("click", listener); runButton.addEventListener("click", async (event) => {
runButton.blur();
await handler.handleEvent(event);
});
return runButton; return runButton;
}; };
const makeEditorDiv = (listener, type) => { const makeEditorDiv = (handler, type) => {
const editorDiv = document.createElement("div"); const editorDiv = document.createElement("div");
editorDiv.className = `${type}-editor-input`; editorDiv.className = `${type}-editor-input`;
editorDiv.setAttribute("aria-label", "Python Script Area"); editorDiv.setAttribute("aria-label", "Python Script Area");
const runButton = makeRunButton(listener, type); const runButton = makeRunButton(handler, type);
const editorShadowContainer = document.createElement("div"); const editorShadowContainer = document.createElement("div");
// avoid outer elements intercepting key events (reveal as example) // avoid outer elements intercepting key events (reveal as example)
@@ -120,15 +123,15 @@ const makeOutDiv = (type) => {
return outDiv; return outDiv;
}; };
const makeBoxDiv = (listener, type) => { const makeBoxDiv = (handler, type) => {
const boxDiv = document.createElement("div"); const boxDiv = document.createElement("div");
boxDiv.className = `${type}-editor-box`; boxDiv.className = `${type}-editor-box`;
const editorDiv = makeEditorDiv(listener, type); const editorDiv = makeEditorDiv(handler, type);
const outDiv = makeOutDiv(type); const outDiv = makeOutDiv(type);
boxDiv.append(editorDiv, outDiv); boxDiv.append(editorDiv, outDiv);
return [boxDiv, outDiv]; return [boxDiv, outDiv, editorDiv.querySelector("button")];
}; };
const init = async (script, type, interpreter) => { const init = async (script, type, interpreter) => {
@@ -138,7 +141,7 @@ const init = async (script, type, interpreter) => {
{ python }, { python },
{ indentUnit }, { indentUnit },
{ keymap }, { keymap },
{ defaultKeymap }, { defaultKeymap, indentWithTab },
] = await Promise.all([ ] = await Promise.all([
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"), import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"), import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
@@ -168,6 +171,8 @@ const init = async (script, type, interpreter) => {
? await fetch(script.src).then((b) => b.text()) ? await fetch(script.src).then((b) => b.text())
: script.textContent; : script.textContent;
const context = { const context = {
// allow the listener to be overridden at distance
handleEvent: execute,
interpreter, interpreter,
env, env,
config: config:
@@ -184,6 +189,29 @@ const init = async (script, type, interpreter) => {
let target; let target;
defineProperties(script, { defineProperties(script, {
target: { get: () => target }, target: { get: () => target },
handleEvent: {
get: () => context.handleEvent,
set: (callback) => {
// do not bother with logic if it was set back as its original handler
if (callback === execute) context.handleEvent = execute;
// in every other case be sure that if the listener override returned
// `false` nothing happens, otherwise keep doing what it always did
else {
context.handleEvent = async (event) => {
// trap the currentTarget ASAP (if any)
// otherwise it gets lost asynchronously
const { currentTarget } = event;
// augment a code snapshot before invoking the override
defineProperties(event, {
code: { value: context.pySrc },
});
// avoid executing the default handler if the override returned `false`
if ((await callback(event)) !== false)
await execute.call(context, { currentTarget });
};
}
},
},
code: { code: {
get: () => context.pySrc, get: () => context.pySrc,
set: (insert) => { set: (insert) => {
@@ -214,8 +242,8 @@ const init = async (script, type, interpreter) => {
isSetup = wasSetup; isSetup = wasSetup;
source = wasSource; source = wasSource;
}; };
return execute return context
.call(context, { currentTarget: null }) .handleEvent({ currentTarget: null })
.then(restore, restore); .then(restore, restore);
}, },
}, },
@@ -227,7 +255,7 @@ const init = async (script, type, interpreter) => {
}; };
if (isSetup) { if (isSetup) {
await execute.call(context, { currentTarget: null }); await context.handleEvent({ currentTarget: null });
notify(); notify();
return; return;
} }
@@ -250,8 +278,7 @@ const init = async (script, type, interpreter) => {
if (!target.hasAttribute("root")) target.setAttribute("root", target.id); if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js // @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
const listener = execute.bind(context); const [boxDiv, outDiv, runButton] = makeBoxDiv(context, type);
const [boxDiv, outDiv] = makeBoxDiv(listener, type);
boxDiv.dataset.env = script.hasAttribute("env") ? env : interpreter; boxDiv.dataset.env = script.hasAttribute("env") ? env : interpreter;
const inputChild = boxDiv.querySelector(`.${type}-editor-input > div`); const inputChild = boxDiv.querySelector(`.${type}-editor-input > div`);
@@ -264,8 +291,9 @@ const init = async (script, type, interpreter) => {
const doc = dedent(script.textContent).trim(); const doc = dedent(script.textContent).trim();
// preserve user indentation, if any // preserve user indentation, if any
const indentation = /^(\s+)/m.test(doc) ? RegExp.$1 : " "; const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
const listener = () => runButton.click();
const editor = new EditorView({ const editor = new EditorView({
extensions: [ extensions: [
indentUnit.of(indentation), indentUnit.of(indentation),
@@ -275,9 +303,13 @@ const init = async (script, type, interpreter) => {
{ key: "Ctrl-Enter", run: listener, preventDefault: true }, { key: "Ctrl-Enter", run: listener, preventDefault: true },
{ key: "Cmd-Enter", run: listener, preventDefault: true }, { key: "Cmd-Enter", run: listener, preventDefault: true },
{ key: "Shift-Enter", run: listener, preventDefault: true }, { key: "Shift-Enter", run: listener, preventDefault: true },
// @see https://codemirror.net/examples/tab/
indentWithTab,
]), ]),
basicSetup, basicSetup,
], ],
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
parent, parent,
doc, doc,
}); });

View File

@@ -43,6 +43,7 @@ from pyscript.magic_js import (
sync, sync,
window, window,
) )
from pyscript.storage import Storage, storage
from pyscript.websocket import WebSocket from pyscript.websocket import WebSocket
try: try:

View File

@@ -0,0 +1,148 @@
# https://www.npmjs.com/package/flatted
import json as _json
class _Known:
def __init__(self):
self.key = []
self.value = []
class _String:
def __init__(self, value):
self.value = value
def _array_keys(value):
keys = []
i = 0
for _ in value:
keys.append(i)
i += 1
return keys
def _object_keys(value):
keys = []
for key in value:
keys.append(key)
return keys
def _is_array(value):
return isinstance(value, list) or isinstance(value, tuple)
def _is_object(value):
return isinstance(value, dict)
def _is_string(value):
return isinstance(value, str)
def _index(known, input, value):
input.append(value)
index = str(len(input) - 1)
known.key.append(value)
known.value.append(index)
return index
def _loop(keys, input, known, output):
for key in keys:
value = output[key]
if isinstance(value, _String):
_ref(key, input[int(value.value)], input, known, output)
return output
def _ref(key, value, input, known, output):
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 not value in known:
known.append(value)
value = _loop(_object_keys(value), input, known, value)
output[key] = value
def _relate(known, input, value):
if _is_string(value) or _is_array(value) or _is_object(value):
try:
return known.value[known.key.index(value)]
except:
return _index(known, input, value)
return value
def _transform(known, input, value):
if _is_array(value):
output = []
for val in value:
output.append(_relate(known, input, val))
return output
if _is_object(value):
obj = {}
for key in value:
obj[key] = _relate(known, input, value[key])
return obj
return value
def _wrap(value):
if _is_string(value):
return _String(value)
if _is_array(value):
i = 0
for val in value:
value[i] = _wrap(val)
i += 1
elif _is_object(value):
for key in value:
value[key] = _wrap(value[key])
return value
def parse(value, *args, **kwargs):
json = _json.loads(value, *args, **kwargs)
wrapped = []
for value in json:
wrapped.append(_wrap(value))
input = []
for value in wrapped:
if isinstance(value, _String):
input.append(value.value)
else:
input.append(value)
value = input[0]
if _is_array(value):
return _loop(_array_keys(value), input, [value], value)
if _is_object(value):
return _loop(_object_keys(value), input, [value], value)
return value
def stringify(value, *args, **kwargs):
known = _Known()
input = []
output = []
i = int(_index(known, input, value))
while i < len(input):
output.append(_transform(known, input, input[i]))
i += 1
return _json.dumps(output, *args, **kwargs)

View File

@@ -0,0 +1,60 @@
from polyscript import storage as _storage
from pyscript.flatted import parse as _parse
from pyscript.flatted import stringify as _stringify
# convert a Python value into an IndexedDB compatible entry
def _to_idb(value):
if value is None:
return _stringify(["null", 0])
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
return _stringify(["generic", value])
if isinstance(value, bytearray):
return _stringify(["bytearray", [v for v in value]])
if isinstance(value, memoryview):
return _stringify(["memoryview", [v for v in value]])
raise TypeError(f"Unexpected value: {value}")
# convert an IndexedDB compatible entry into a Python value
def _from_idb(value):
(
kind,
result,
) = _parse(value)
if kind == "null":
return None
if kind == "generic":
return result
if kind == "bytearray":
return bytearray(result)
if kind == "memoryview":
return memoryview(bytearray(result))
return value
class Storage(dict):
def __init__(self, store):
super().__init__({k: _from_idb(v) for k, v in store.entries()})
self.__store__ = store
def __delitem__(self, attr):
self.__store__.delete(attr)
super().__delitem__(attr)
def __setitem__(self, attr, value):
self.__store__.set(attr, _to_idb(value))
super().__setitem__(attr, value)
def clear(self):
self.__store__.clear()
super().clear()
async def sync(self):
await self.__store__.sync()
async def storage(name="", storage_class=Storage):
if not name:
raise ValueError("The storage name must be defined")
return storage_class(await _storage(f"@pyscript/{name}"))

View File

@@ -0,0 +1,5 @@
from . import elements
# Ugly trick to hide the dom module in the web package since we want the module
# to allow querying right away.
from .dom import dom

View File

@@ -0,0 +1,21 @@
from pyscript import document
from pyscript.web.elements import Element, ElementCollection
class PyDom:
# Add objects we want to expose to the DOM namespace since this class instance is being
# remapped as "the module" itself
ElementCollection = ElementCollection
def __init__(self):
self._js = document
self.body = Element(document.body)
self.head = Element(document.head)
def __getitem__(self, key):
elements = self._js.querySelectorAll(key)
return ElementCollection([Element(el) for el in elements])
dom = PyDom()

File diff suppressed because it is too large Load Diff

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

@@ -88,3 +88,8 @@ test('MicroPython + Pyodide ffi', async ({ page }) => {
await page.goto('http://localhost:8080/test/ffi.html'); await page.goto('http://localhost:8080/test/ffi.html');
await page.waitForSelector('html.mpy.py'); await page.waitForSelector('html.mpy.py');
}); });
test('MicroPython + Storage', async ({ page }) => {
await page.goto('http://localhost:8080/test/storage.html');
await page.waitForSelector('html.ok');
});

View File

@@ -10,6 +10,8 @@
<body> <body>
<script type="mpy" src="pydom.py"></script> <script type="mpy" src="pydom.py"></script>
<div id="system-info"></div>
<button id="just-a-button">Click For Time</button> <button id="just-a-button">Click For Time</button>
<button id="color-button">Click For Color</button> <button id="color-button">Click For Color</button>
<button id="color-reset-button">Reset Color</button> <button id="color-reset-button">Reset Color</button>

View File

@@ -0,0 +1,46 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@pyscript/core storage</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<script type="mpy" async>
from random import random
from pyscript import storage
store = await storage(name="test")
print("before", len(store))
for k in store:
if isinstance(store[k], memoryview):
print(f" {k}: {store[k].hex()} as hex()")
else:
print(f" {k}: {store[k]}")
store["ba"] = bytearray([0, 1, 2, 3, 4])
store["mv"] = memoryview(bytearray([5, 6, 7, 8, 9]))
store["random"] = ("some", random(), True)
store["key"] = "value"
print("now", len(store))
for k in store:
print(f" {k}: {store[k]}")
del store["key"]
# store.clear()
print("after", len(store))
for k in store:
print(f" {k}: {store[k]}")
await store.sync()
import js
js.document.documentElement.classList.add("ok")
</script>
</body>
</html>

View File

@@ -5,8 +5,16 @@ declare namespace _default {
"event_handling.py": string; "event_handling.py": string;
"fetch.py": string; "fetch.py": string;
"ffi.py": string; "ffi.py": string;
"flatted.py": string;
"magic_js.py": string; "magic_js.py": string;
"storage.py": string;
"util.py": string; "util.py": string;
web: {
"__init__.py": string;
"dom.py": string;
"elements.py": string;
"media.py": string;
};
"websocket.py": string; "websocket.py": string;
}; };
let pyweb: { let pyweb: {