mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
remove PyWidget and py-register-widget + refactor PyList as a Python Plugin (#1452)
* remove PyWidget and py-register-widget * refactor py-list as Plugin * add newline * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix eslint * handle case if src not supplied * move inside if * - Remove src attribute for py-list - Re-implement as a Python plugin - Remove pylist.py from examples directory - Remove PyListPlugin as one of the default ones * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * move PyItem and PyList classes to the plugin * clean up PyListTemplate and PyItemTemplate from pyscript module * fix linting * use PyList instead of PyListTemplate instead * fix example for todo-pylist * re-enable and improve test * move py-list plugin to examples * fix py-list plugin link --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
170
examples/py_list.py
Normal file
170
examples/py_list.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import time
|
||||
from datetime import datetime as dt
|
||||
from textwrap import dedent
|
||||
|
||||
import js
|
||||
from pyscript import Element, Plugin, create
|
||||
|
||||
plugin = Plugin("PyList")
|
||||
|
||||
|
||||
class PyItemTemplate(Element):
|
||||
label_fields = None
|
||||
|
||||
def __init__(self, data, labels=None, state_key=None, parent=None):
|
||||
self.data = data
|
||||
|
||||
self.register_parent(parent)
|
||||
|
||||
if not labels:
|
||||
labels = list(self.data.keys())
|
||||
self.labels = labels
|
||||
|
||||
self.state_key = state_key
|
||||
|
||||
super().__init__(self._id)
|
||||
|
||||
def register_parent(self, parent):
|
||||
self._parent = parent
|
||||
if parent:
|
||||
self._id = f"{self._parent._id}-c-{len(self._parent._children)}"
|
||||
self.data["id"] = self._id
|
||||
else:
|
||||
self._id = None
|
||||
|
||||
def create(self):
|
||||
new_child = create("div", self._id, "py-li-element")
|
||||
new_child._element.innerHTML = dedent(
|
||||
f"""
|
||||
<label id="{self._id}" for="flex items-center p-2 ">
|
||||
<input class="mr-2" type="checkbox" class="task-check">
|
||||
<p>{self.render_content()}</p>
|
||||
</label>
|
||||
"""
|
||||
)
|
||||
return new_child
|
||||
|
||||
def on_click(self, evt):
|
||||
pass
|
||||
|
||||
def pre_append(self):
|
||||
pass
|
||||
|
||||
def post_append(self):
|
||||
self.element.click = self.on_click
|
||||
self.element.onclick = self.on_click
|
||||
self._post_append()
|
||||
|
||||
def _post_append(self):
|
||||
pass
|
||||
|
||||
def strike(self, value, extra=None):
|
||||
if value:
|
||||
self.add_class("line-through")
|
||||
else:
|
||||
self.remove_class("line-through")
|
||||
|
||||
def render_content(self):
|
||||
return " - ".join([self.data[f] for f in self.labels])
|
||||
|
||||
|
||||
class PyListTemplate:
|
||||
item_class = PyItemTemplate
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self._children = []
|
||||
self._id = self.parent.id
|
||||
self.main_style_classes = "py-li-element"
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return [c.data for c in self._children]
|
||||
|
||||
def render_children(self):
|
||||
binds = {}
|
||||
for i, c in enumerate(self._children):
|
||||
txt = c.element.innerHTML
|
||||
rnd = str(time.time()).replace(".", "")[-5:]
|
||||
new_id = f"{c.element.id}-{i}-{rnd}"
|
||||
binds[new_id] = c.element.id
|
||||
txt = txt.replace(">", f" id='{new_id}'>")
|
||||
print(txt)
|
||||
|
||||
def foo(evt):
|
||||
evtEl = evt.srcElement
|
||||
srcEl = Element(binds[evtEl.id])
|
||||
srcEl.element.onclick()
|
||||
evtEl.classList = srcEl.element.classList
|
||||
|
||||
for new_id in binds:
|
||||
Element(new_id).element.onclick = foo
|
||||
|
||||
def connect(self):
|
||||
self.md = main_div = js.document.createElement("div")
|
||||
main_div.id = self._id + "-list-tasks-container"
|
||||
|
||||
if self.main_style_classes:
|
||||
for klass in self.main_style_classes.split(" "):
|
||||
main_div.classList.add(klass)
|
||||
|
||||
self.parent.appendChild(main_div)
|
||||
|
||||
def add(self, *args, **kws):
|
||||
if not isinstance(args[0], self.item_class):
|
||||
child = self.item_class(*args, **kws)
|
||||
else:
|
||||
child = args[0]
|
||||
child.register_parent(self)
|
||||
return self._add(child)
|
||||
|
||||
def _add(self, child_elem):
|
||||
self.pre_child_append(child_elem)
|
||||
child_elem.pre_append()
|
||||
self._children.append(child_elem)
|
||||
self.md.appendChild(child_elem.create().element)
|
||||
child_elem.post_append()
|
||||
self.child_appended(child_elem)
|
||||
return child_elem
|
||||
|
||||
def pre_child_append(self, child):
|
||||
pass
|
||||
|
||||
def child_appended(self, child):
|
||||
"""Overwrite me to define logic"""
|
||||
pass
|
||||
|
||||
|
||||
class PyItem(PyItemTemplate):
|
||||
def on_click(self, evt=None):
|
||||
self.data["done"] = not self.data["done"]
|
||||
self.strike(self.data["done"])
|
||||
|
||||
self.select("input").element.checked = self.data["done"]
|
||||
|
||||
|
||||
class PyList(PyListTemplate):
|
||||
item_class = PyItem
|
||||
|
||||
def add(self, item):
|
||||
if isinstance(item, str):
|
||||
item = {"content": item, "done": False, "created_at": dt.now()}
|
||||
|
||||
super().add(item, labels=["content"], state_key="done")
|
||||
|
||||
|
||||
@plugin.register_custom_element("py-list")
|
||||
class PyListPlugin:
|
||||
def __init__(self, element):
|
||||
self.element = element
|
||||
self.py_list = PyList(self.element)
|
||||
|
||||
def add(self, item):
|
||||
self.py_list.add(item)
|
||||
|
||||
def connect(self):
|
||||
self.py_list.connect()
|
||||
@@ -1,21 +0,0 @@
|
||||
from datetime import datetime as dt
|
||||
|
||||
import pyscript
|
||||
|
||||
|
||||
class PyItem(pyscript.PyItemTemplate):
|
||||
def on_click(self, evt=None):
|
||||
self.data["done"] = not self.data["done"]
|
||||
self.strike(self.data["done"])
|
||||
|
||||
self.select("input").element.checked = self.data["done"]
|
||||
|
||||
|
||||
class PyList(pyscript.PyListTemplate):
|
||||
item_class = PyItem
|
||||
|
||||
def add(self, item):
|
||||
if isinstance(item, str):
|
||||
item = {"content": item, "done": False, "created_at": dt.now()}
|
||||
|
||||
super().add(item, labels=["content"], state_key="done")
|
||||
@@ -28,23 +28,19 @@
|
||||
</nav>
|
||||
<section class="pyscript">
|
||||
<h1>To Do List</h1>
|
||||
<py-tutor modules="utils.py;pylist.py">
|
||||
<py-register-widget
|
||||
src="./pylist.py"
|
||||
name="py-list"
|
||||
klass="PyList"
|
||||
></py-register-widget>
|
||||
|
||||
<py-tutor modules="utils.py">
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py",
|
||||
"./py_list.py"
|
||||
]
|
||||
[[fetch]]
|
||||
files = ["./utils.py", "./pylist.py"]
|
||||
files = ["./utils.py"]
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
from js import document
|
||||
from datetime import datetime as dt
|
||||
from pyodide.ffi.wrappers import add_event_listener
|
||||
|
||||
def add_task(*args, **kws):
|
||||
@@ -54,7 +50,8 @@
|
||||
|
||||
# add a new task to the list and tell it to use the `content` key to show in the UI
|
||||
# and to use the key `done` to sync the task status with a checkbox element in the UI
|
||||
myList.add(task)
|
||||
myList = Element("myList")
|
||||
myList.element.pyElementInstance.add(task)
|
||||
|
||||
# clear the inputbox element used to create the new task
|
||||
new_task_content.clear()
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
<py-tutor modules="./utils.py;./todo.py">
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py"
|
||||
"https://pyscript.net/latest/plugins/python/py_tutor.py",
|
||||
"./py_list.py"
|
||||
]
|
||||
[[fetch]]
|
||||
files = ["./utils.py", "./todo.py"]
|
||||
|
||||
@@ -12,8 +12,6 @@ skip = "pyscriptjs/node_modules/*,*.js,*.json"
|
||||
[tool.ruff]
|
||||
builtins = [
|
||||
"Element",
|
||||
"PyItemTemplate",
|
||||
"PyListTemplate",
|
||||
"pyscript",
|
||||
]
|
||||
ignore = [
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { InterpreterClient } from '../interpreter_client';
|
||||
import type { PyScriptApp } from '../main';
|
||||
import { make_PyRepl } from './pyrepl';
|
||||
import { make_PyWidget } from './pywidget';
|
||||
|
||||
function createCustomElements(interpreter: InterpreterClient, app: PyScriptApp) {
|
||||
const PyWidget = make_PyWidget(interpreter);
|
||||
const PyRepl = make_PyRepl(interpreter, app);
|
||||
|
||||
customElements.define('py-repl', PyRepl);
|
||||
customElements.define('py-register-widget', PyWidget);
|
||||
}
|
||||
|
||||
export { createCustomElements };
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { PyProxy, PyProxyCallable } from 'pyodide';
|
||||
import { getLogger } from '../logger';
|
||||
import { robustFetch } from '../fetch';
|
||||
import { InterpreterClient } from '../interpreter_client';
|
||||
import type { Remote } from 'synclink';
|
||||
|
||||
const logger = getLogger('py-register-widget');
|
||||
|
||||
function createWidget(interpreter: InterpreterClient, name: string, code: string, klass: string) {
|
||||
class CustomWidget extends HTMLElement {
|
||||
wrapper: HTMLElement;
|
||||
|
||||
name: string = name;
|
||||
klass: string = klass;
|
||||
code: string = code;
|
||||
proxy: Remote<PyProxy & { connect(): void }>;
|
||||
proxyClass: Remote<PyProxyCallable>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.wrapper = document.createElement('slot');
|
||||
this.attachShadow({ mode: 'open' }).appendChild(this.wrapper);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await interpreter.runButDontRaise(this.code);
|
||||
this.proxyClass = (await interpreter.globals.get(this.klass)) as Remote<PyProxyCallable>;
|
||||
this.proxy = (await this.proxyClass(this)) as Remote<PyProxy & { connect(): void }>;
|
||||
await this.proxy.connect();
|
||||
await this.registerWidget();
|
||||
}
|
||||
|
||||
async registerWidget() {
|
||||
logger.info('new widget registered:', this.name);
|
||||
await interpreter.globals.set(this.id, this.proxy);
|
||||
}
|
||||
}
|
||||
customElements.define(name, CustomWidget);
|
||||
}
|
||||
|
||||
export function make_PyWidget(interpreter: InterpreterClient) {
|
||||
class PyWidget extends HTMLElement {
|
||||
name: string;
|
||||
klass: string;
|
||||
outputElement: HTMLElement;
|
||||
errorElement: HTMLElement;
|
||||
wrapper: HTMLElement;
|
||||
theme: string;
|
||||
source: string;
|
||||
code: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.wrapper = document.createElement('slot');
|
||||
this.attachShadow({ mode: 'open' }).appendChild(this.wrapper);
|
||||
|
||||
this.addAttributes('src', 'name', 'klass');
|
||||
}
|
||||
|
||||
addAttributes(...attrs: string[]) {
|
||||
for (const each of attrs) {
|
||||
const property = each === 'src' ? 'source' : each;
|
||||
if (this.hasAttribute(each)) {
|
||||
this[property] = this.getAttribute(each);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
if (this.id === undefined) {
|
||||
throw new ReferenceError(
|
||||
`No id specified for component. Components must have an explicit id. Please use id="" to specify your component id.`,
|
||||
);
|
||||
}
|
||||
|
||||
const mainDiv = document.createElement('div');
|
||||
mainDiv.id = this.id + '-main';
|
||||
this.appendChild(mainDiv);
|
||||
logger.debug('PyWidget: reading source', this.source);
|
||||
this.code = await this.getSourceFromFile(this.source);
|
||||
createWidget(interpreter, this.name, this.code, this.klass);
|
||||
}
|
||||
|
||||
async getSourceFromFile(s: string): Promise<string> {
|
||||
const response = await robustFetch(s);
|
||||
return await response.text();
|
||||
}
|
||||
}
|
||||
|
||||
return PyWidget;
|
||||
}
|
||||
@@ -6,9 +6,6 @@ from ._event_loop import run_until_complete
|
||||
from ._html import (
|
||||
HTML,
|
||||
Element,
|
||||
PyItemTemplate,
|
||||
PyListTemplate,
|
||||
PyWidgetTheme,
|
||||
add_classes,
|
||||
create,
|
||||
display,
|
||||
@@ -43,9 +40,6 @@ __all__ = [
|
||||
"Element",
|
||||
"add_classes",
|
||||
"create",
|
||||
"PyWidgetTheme",
|
||||
"PyItemTemplate",
|
||||
"PyListTemplate",
|
||||
"run_until_complete",
|
||||
"loop",
|
||||
"Plugin",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import time
|
||||
from textwrap import dedent
|
||||
|
||||
import js
|
||||
@@ -148,143 +147,3 @@ def create(what, id_=None, classes=""):
|
||||
element.id = id_
|
||||
add_classes(element, classes)
|
||||
return Element(id_, element)
|
||||
|
||||
|
||||
class PyWidgetTheme:
|
||||
def __init__(self, main_style_classes):
|
||||
self.main_style_classes = main_style_classes
|
||||
|
||||
def theme_it(self, widget):
|
||||
for klass in self.main_style_classes.split(" "):
|
||||
widget.classList.add(klass)
|
||||
|
||||
|
||||
class PyItemTemplate(Element):
|
||||
label_fields = None
|
||||
|
||||
def __init__(self, data, labels=None, state_key=None, parent=None):
|
||||
self.data = data
|
||||
|
||||
self.register_parent(parent)
|
||||
|
||||
if not labels:
|
||||
labels = list(self.data.keys())
|
||||
self.labels = labels
|
||||
|
||||
self.state_key = state_key
|
||||
|
||||
super().__init__(self._id)
|
||||
|
||||
def register_parent(self, parent):
|
||||
self._parent = parent
|
||||
if parent:
|
||||
self._id = f"{self._parent._id}-c-{len(self._parent._children)}"
|
||||
self.data["id"] = self._id
|
||||
else:
|
||||
self._id = None
|
||||
|
||||
def create(self):
|
||||
new_child = create("div", self._id, "py-li-element")
|
||||
new_child._element.innerHTML = dedent(
|
||||
f"""
|
||||
<label id="{self._id}" for="flex items-center p-2 ">
|
||||
<input class="mr-2" type="checkbox" class="task-check">
|
||||
<p>{self.render_content()}</p>
|
||||
</label>
|
||||
"""
|
||||
)
|
||||
return new_child
|
||||
|
||||
def on_click(self, evt):
|
||||
pass
|
||||
|
||||
def pre_append(self):
|
||||
pass
|
||||
|
||||
def post_append(self):
|
||||
self.element.click = self.on_click
|
||||
self.element.onclick = self.on_click
|
||||
|
||||
self._post_append()
|
||||
|
||||
def _post_append(self):
|
||||
pass
|
||||
|
||||
def strike(self, value, extra=None):
|
||||
if value:
|
||||
self.add_class("line-through")
|
||||
else:
|
||||
self.remove_class("line-through")
|
||||
|
||||
def render_content(self):
|
||||
return " - ".join([self.data[f] for f in self.labels])
|
||||
|
||||
|
||||
class PyListTemplate:
|
||||
theme = PyWidgetTheme("py-li-element")
|
||||
item_class = PyItemTemplate
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self._children = []
|
||||
self._id = self.parent.id
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return [c.data for c in self._children]
|
||||
|
||||
def render_children(self):
|
||||
binds = {}
|
||||
for i, c in enumerate(self._children):
|
||||
txt = c.element.innerHTML
|
||||
rnd = str(time.time()).replace(".", "")[-5:]
|
||||
new_id = f"{c.element.id}-{i}-{rnd}"
|
||||
binds[new_id] = c.element.id
|
||||
txt = txt.replace(">", f" id='{new_id}'>")
|
||||
print(txt)
|
||||
|
||||
def foo(evt):
|
||||
evtEl = evt.srcElement
|
||||
srcEl = Element(binds[evtEl.id])
|
||||
srcEl.element.onclick()
|
||||
evtEl.classList = srcEl.element.classList
|
||||
|
||||
for new_id in binds:
|
||||
Element(new_id).element.onclick = foo
|
||||
|
||||
def connect(self):
|
||||
self.md = main_div = js.document.createElement("div")
|
||||
main_div.id = self._id + "-list-tasks-container"
|
||||
|
||||
if self.theme:
|
||||
self.theme.theme_it(main_div)
|
||||
|
||||
self.parent.appendChild(main_div)
|
||||
|
||||
def add(self, *args, **kws):
|
||||
if not isinstance(args[0], self.item_class):
|
||||
child = self.item_class(*args, **kws)
|
||||
else:
|
||||
child = args[0]
|
||||
child.register_parent(self)
|
||||
return self._add(child)
|
||||
|
||||
def _add(self, child_elem):
|
||||
self.pre_child_append(child_elem)
|
||||
child_elem.pre_append()
|
||||
self._children.append(child_elem)
|
||||
self.md.appendChild(child_elem.create().element)
|
||||
child_elem.post_append()
|
||||
self.child_appended(child_elem)
|
||||
return child_elem
|
||||
|
||||
def pre_child_append(self, child):
|
||||
pass
|
||||
|
||||
def child_appended(self, child):
|
||||
"""Overwrite me to define logic"""
|
||||
pass
|
||||
|
||||
@@ -360,15 +360,31 @@ class TestExamples(PyScriptTest):
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["./utils.py", "./todo.py"])
|
||||
|
||||
@pytest.mark.xfail(reason="fails after introducing synclink, fix me soon!")
|
||||
def test_todo_pylist(self):
|
||||
# XXX improve this test
|
||||
self.goto("examples/todo-pylist.html")
|
||||
self.wait_for_pyscript()
|
||||
assert self.page.title() == "Todo App"
|
||||
wait_for_render(self.page, "*", "<input.*?id=['\"]new-task-content['\"].*?>")
|
||||
todo_input = self.page.locator("input")
|
||||
submit_task_button = self.page.locator("button#new-task-btn")
|
||||
|
||||
todo_input.type("Fold laundry")
|
||||
submit_task_button.click()
|
||||
|
||||
first_task = self.page.locator("div#myList-c-0")
|
||||
assert "Fold laundry" in first_task.inner_text()
|
||||
|
||||
task_checkbox = first_task.locator("input")
|
||||
# Confirm that the new task isn't checked
|
||||
assert not task_checkbox.is_checked()
|
||||
|
||||
# Let's mark it as done now
|
||||
task_checkbox.check()
|
||||
|
||||
# Basic check that the task has the line-through class
|
||||
assert "line-through" in first_task.get_attribute("class")
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["utils.py", "pylist.py"])
|
||||
self.check_tutor_generated_code(modules_to_check=["utils.py"])
|
||||
|
||||
@pytest.mark.xfail(reason="To be moved to collective and updated, see issue #686")
|
||||
def test_toga_freedom(self):
|
||||
|
||||
Reference in New Issue
Block a user