mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 10:17:23 -05:00
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>
This commit is contained in:
@@ -43,6 +43,8 @@ from pyscript.magic_js import (
|
||||
try:
|
||||
from pyscript.event_handling import when
|
||||
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
|
||||
|
||||
when = NotSupported(
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import inspect
|
||||
|
||||
from pyodide.ffi.wrappers import add_event_listener
|
||||
try:
|
||||
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
|
||||
|
||||
|
||||
@@ -27,19 +35,32 @@ def when(event_type=None, selector=None):
|
||||
f"Invalid selector: {selector}. Selector must"
|
||||
" be a string, a pydom.Element or a pydom.ElementCollection."
|
||||
)
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
# Function doesn't receive events
|
||||
if not sig.parameters:
|
||||
|
||||
sig = inspect.signature(func)
|
||||
# Function doesn't receive events
|
||||
if not sig.parameters:
|
||||
def wrapper(*args, **kwargs):
|
||||
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):
|
||||
func()
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except TypeError as e:
|
||||
if "takes 0 positional arguments" in str(e):
|
||||
return func()
|
||||
|
||||
raise
|
||||
|
||||
for el in elements:
|
||||
add_event_listener(el, event_type, wrapper)
|
||||
|
||||
for el in elements:
|
||||
add_event_listener(el, event_type, wrapper)
|
||||
else:
|
||||
for el in elements:
|
||||
add_event_listener(el, event_type, func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
1
pyscript.core/src/stdlib/pyweb/__init__.py
Normal file
1
pyscript.core/src/stdlib/pyweb/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .pydom import dom as pydom
|
||||
@@ -1,9 +1,34 @@
|
||||
import sys
|
||||
import warnings
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
try:
|
||||
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
|
||||
except ImportError:
|
||||
# TODO: same comment about micropython as above
|
||||
def JsProxy(obj):
|
||||
return obj
|
||||
|
||||
|
||||
from pyodide.ffi import JsProxy
|
||||
from pyscript import display, document, window
|
||||
|
||||
alert = window.alert
|
||||
@@ -361,7 +386,7 @@ class OptionsProxy:
|
||||
return self.options[key]
|
||||
|
||||
|
||||
class StyleProxy(dict):
|
||||
class StyleProxy: # (dict):
|
||||
def __init__(self, element: Element) -> None:
|
||||
self._element = element
|
||||
|
||||
@@ -480,7 +505,7 @@ class ElementCollection:
|
||||
|
||||
|
||||
class DomScope:
|
||||
def __getattr__(self, __name: str) -> Any:
|
||||
def __getattr__(self, __name: str):
|
||||
element = document[f"#{__name}"]
|
||||
if element:
|
||||
return element[0]
|
||||
@@ -494,7 +519,12 @@ class PyDom(BaseElement):
|
||||
ElementCollection = ElementCollection
|
||||
|
||||
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.body = Element(document.body)
|
||||
self.head = Element(document.head)
|
||||
@@ -503,10 +533,6 @@ class PyDom(BaseElement):
|
||||
return super().create(type_, is_child=False, classes=classes, html=html)
|
||||
|
||||
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)
|
||||
if not elements:
|
||||
return None
|
||||
@@ -514,5 +540,3 @@ class PyDom(BaseElement):
|
||||
|
||||
|
||||
dom = PyDom()
|
||||
|
||||
sys.modules[__name__] = dom
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime as dt
|
||||
|
||||
from pyscript import display
|
||||
from pyscript import display, when
|
||||
from pyweb import pydom
|
||||
from pyweb.base import when
|
||||
|
||||
|
||||
@when("click", "#just-a-button")
|
||||
def on_click(event):
|
||||
print(f"Hello from Python! {dt.now()}")
|
||||
display(f"Hello from Python! {dt.now()}", append=False, target="result")
|
||||
def on_click():
|
||||
try:
|
||||
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")
|
||||
def on_color_click(event):
|
||||
print("1")
|
||||
btn = pydom["#result"]
|
||||
print("2")
|
||||
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"
|
||||
|
||||
|
||||
|
||||
19
pyscript.core/test/pydom_mp.html
Normal file
19
pyscript.core/test/pydom_mp.html
Normal 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>
|
||||
@@ -1,6 +1,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>PyperCard PyTest Suite</title>
|
||||
<title>PyDom Test Suite</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
@@ -32,7 +32,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<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>
|
||||
<p>You can pass test parameters to this test suite by passing them as query params on the url.
|
||||
|
||||
@@ -336,7 +336,7 @@ class TestSelect:
|
||||
assert select.options[0].html == "Option 1"
|
||||
|
||||
# WHEN we add another option (blank this time)
|
||||
select.options.add()
|
||||
select.options.add("")
|
||||
|
||||
# EXPECT the select element to have 2 options
|
||||
assert len(select.options) == 2
|
||||
|
||||
@@ -17,6 +17,7 @@ from playwright.sync_api import Error as PlaywrightError
|
||||
|
||||
ROOT = py.path.local(__file__).dirpath("..", "..", "..")
|
||||
BUILD = ROOT.join("pyscript.core").join("dist")
|
||||
TEST = ROOT.join("pyscript.core").join("test")
|
||||
|
||||
|
||||
def params_with_marks(params):
|
||||
@@ -206,6 +207,14 @@ class PyScriptTest:
|
||||
self.tmpdir = tmpdir
|
||||
# create a symlink to BUILD inside tmpdir
|
||||
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.join("favicon.ico").write("")
|
||||
self.logger = logger
|
||||
|
||||
30
pyscript.core/tests/integration/test_integration.py
Normal file
30
pyscript.core/tests/integration/test_integration.py
Normal 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
|
||||
1
pyscript.core/types/stdlib/pyscript.d.ts
vendored
1
pyscript.core/types/stdlib/pyscript.d.ts
vendored
@@ -7,6 +7,7 @@ declare namespace _default {
|
||||
"util.py": string;
|
||||
};
|
||||
let pyweb: {
|
||||
"__init__.py": string;
|
||||
"media.py": string;
|
||||
"pydom.py": string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user