Skip to content

Commit

Permalink
PyDom compatibility with MicroPython (#1954)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
fpliger and pre-commit-ci[bot] committed Jan 30, 2024
1 parent 3ff0f84 commit bcaab0e
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 36 deletions.
2 changes: 2 additions & 0 deletions pyscript.core/src/stdlib/pyscript/__init__.py
Expand Up @@ -43,6 +43,8 @@
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(
Expand Down
41 changes: 31 additions & 10 deletions pyscript.core/src/stdlib/pyscript/event_handling.py
@@ -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


Expand All @@ -27,19 +35,32 @@ def decorator(func):
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 change: 1 addition & 0 deletions pyscript.core/src/stdlib/pyweb/__init__.py
@@ -0,0 +1 @@
from .pydom import dom as pydom
52 changes: 38 additions & 14 deletions pyscript.core/src/stdlib/pyweb/pydom.py
@@ -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
Expand Down Expand Up @@ -361,7 +386,7 @@ def __getitem__(self, key):
return self.options[key]


class StyleProxy(dict):
class StyleProxy: # (dict):
def __init__(self, element: Element) -> None:
self._element = element

Expand Down Expand Up @@ -480,7 +505,7 @@ def __repr__(self):


class DomScope:
def __getattr__(self, __name: str) -> Any:
def __getattr__(self, __name: str):
element = document[f"#{__name}"]
if element:
return element[0]
Expand All @@ -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)
Expand All @@ -503,16 +533,10 @@ def create(self, type_, classes=None, html=None):
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
return ElementCollection([Element(el) for el in elements])


dom = PyDom()

sys.modules[__name__] = dom
2 changes: 1 addition & 1 deletion pyscript.core/test/pydom.html
Expand Up @@ -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>
Expand Down
22 changes: 14 additions & 8 deletions pyscript.core/test/pydom.py
@@ -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"


Expand Down
19 changes: 19 additions & 0 deletions pyscript.core/test/pydom_mp.html
@@ -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>
4 changes: 2 additions & 2 deletions pyscript.core/test/pyscript_dom/index.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">
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pyscript.core/test/pyscript_dom/tests/test_dom.py
Expand Up @@ -336,7 +336,7 @@ def test_select_element_add(self):
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
Expand Down
9 changes: 9 additions & 0 deletions pyscript.core/tests/integration/support.py
Expand Up @@ -17,6 +17,7 @@

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):
Expand Down Expand Up @@ -206,6 +207,14 @@ def init(self, request, tmpdir, logger, page, execution_thread):
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
Expand Down
30 changes: 30 additions & 0 deletions pyscript.core/tests/integration/test_integration.py
@@ -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 change: 1 addition & 0 deletions pyscript.core/types/stdlib/pyscript.d.ts
Expand Up @@ -7,6 +7,7 @@ declare namespace _default {
"util.py": string;
};
let pyweb: {
"__init__.py": string;
"media.py": string;
"pydom.py": string;
};
Expand Down

0 comments on commit bcaab0e

Please sign in to comment.