mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -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:
|
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(
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import inspect
|
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
|
from pyscript.magic_js import document
|
||||||
|
|
||||||
|
|
||||||
@@ -27,19 +35,32 @@ 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)
|
||||||
|
# Function doesn't receive events
|
||||||
|
if not sig.parameters:
|
||||||
|
|
||||||
sig = inspect.signature(func)
|
def wrapper(*args, **kwargs):
|
||||||
# Function doesn't receive events
|
func()
|
||||||
if not sig.parameters:
|
|
||||||
|
|
||||||
|
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):
|
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 func
|
||||||
|
|
||||||
return decorator
|
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
|
try:
|
||||||
import warnings
|
from typing import Any
|
||||||
from functools import cached_property
|
except ImportError:
|
||||||
from typing import Any
|
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
|
from pyscript import display, document, window
|
||||||
|
|
||||||
alert = window.alert
|
alert = window.alert
|
||||||
@@ -361,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
|
||||||
|
|
||||||
@@ -480,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]
|
||||||
@@ -494,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)
|
||||||
@@ -503,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
|
||||||
@@ -514,5 +540,3 @@ class PyDom(BaseElement):
|
|||||||
|
|
||||||
|
|
||||||
dom = PyDom()
|
dom = PyDom()
|
||||||
|
|
||||||
sys.modules[__name__] = dom
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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">
|
<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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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;
|
"util.py": string;
|
||||||
};
|
};
|
||||||
let pyweb: {
|
let pyweb: {
|
||||||
|
"__init__.py": string;
|
||||||
"media.py": string;
|
"media.py": string;
|
||||||
"pydom.py": string;
|
"pydom.py": string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user