Compare commits

...

2 Commits

Author SHA1 Message Date
Fabio Pliger
bcaab0eb93 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>
2024-01-30 11:30:16 -08:00
Andrea Giammarchi
3ff0f84391 Update polyscript + coincident to their latest (#1958) 2024-01-30 12:31:44 +01:00
14 changed files with 166 additions and 58 deletions

View File

@@ -1,17 +1,17 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.3.22", "version": "0.3.23",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.3.22", "version": "0.3.23",
"license": "APACHE-2.0", "license": "APACHE-2.0",
"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.6.16", "polyscript": "^0.6.18",
"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"
@@ -970,17 +970,17 @@
} }
}, },
"node_modules/coincident": { "node_modules/coincident": {
"version": "1.1.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/coincident/-/coincident-1.1.0.tgz", "resolved": "https://registry.npmjs.org/coincident/-/coincident-1.1.1.tgz",
"integrity": "sha512-FXl7/KToJmtaWWEHOJljbco6NKuM9Hzo249p5gI+lvmxv1JRUCoS14SP195zeEW2WypBfTARGkmnE9MwJ1j0Yg==", "integrity": "sha512-4i6bejrHuY6aOY8uhq56g/psUK2k99y5hImpavQz7yfPNCXuJkmtRYpvBOguC0mdYXPgQjvoz+Odw/WO6q4okg==",
"dependencies": { "dependencies": {
"@ungap/structured-clone": "^1.2.0", "@ungap/structured-clone": "^1.2.0",
"@ungap/with-resolvers": "^0.1.0", "@ungap/with-resolvers": "^0.1.0",
"gc-hook": "^0.2.5", "gc-hook": "^0.3.0",
"proxy-target": "^3.0.1" "proxy-target": "^3.0.1"
}, },
"optionalDependencies": { "optionalDependencies": {
"ws": "^8.14.2" "ws": "^8.16.0"
} }
}, },
"node_modules/color-convert": { "node_modules/color-convert": {
@@ -1634,9 +1634,9 @@
} }
}, },
"node_modules/gc-hook": { "node_modules/gc-hook": {
"version": "0.2.5", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.2.5.tgz", "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.0.tgz",
"integrity": "sha512-808B9hJ1T7ak4HRYdXgQjDaHexlaUOBuNFuqOnYotxfKjOHTDxAy8r1Oe7LI+KBeb/H6XUBKzuYi626DjxhxIg==" "integrity": "sha512-Qkp0HM3z839Ns0LpXFJBXqClNW23wQo6JpUdJAjuf1/2jB+oUWSOMzeMv2yFq8Ur45z8IWw9hpRhkSjxSt5RWg=="
}, },
"node_modules/generic-names": { "node_modules/generic-names": {
"version": "4.0.0", "version": "4.0.0",
@@ -2403,15 +2403,15 @@
} }
}, },
"node_modules/polyscript": { "node_modules/polyscript": {
"version": "0.6.16", "version": "0.6.18",
"resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.6.16.tgz", "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.6.18.tgz",
"integrity": "sha512-ri7tWBzsujlnltJ5jKjgpVes6eQWOkl9PdU0QS/EkaPAX406rGPE4/nRMQxI2iWoza6LrH5JpdUhgG6YTskEnA==", "integrity": "sha512-naU5tisWCt/a12kl1lIdtphaGUCEDf4mmlzuOcvjayqkmfXUlxLdc9A5VEEZLcleWr5Wtx34aU9IBBG8hfSI6Q==",
"dependencies": { "dependencies": {
"@ungap/structured-clone": "^1.2.0", "@ungap/structured-clone": "^1.2.0",
"@ungap/with-resolvers": "^0.1.0", "@ungap/with-resolvers": "^0.1.0",
"basic-devtools": "^0.1.6", "basic-devtools": "^0.1.6",
"codedent": "^0.1.2", "codedent": "^0.1.2",
"coincident": "^1.1.0", "coincident": "^1.1.1",
"gc-hook": "^0.3.0", "gc-hook": "^0.3.0",
"html-escaper": "^3.0.3", "html-escaper": "^3.0.3",
"proxy-target": "^3.0.1", "proxy-target": "^3.0.1",
@@ -2419,11 +2419,6 @@
"to-json-callback": "^0.1.1" "to-json-callback": "^0.1.1"
} }
}, },
"node_modules/polyscript/node_modules/gc-hook": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.0.tgz",
"integrity": "sha512-Qkp0HM3z839Ns0LpXFJBXqClNW23wQo6JpUdJAjuf1/2jB+oUWSOMzeMv2yFq8Ur45z8IWw9hpRhkSjxSt5RWg=="
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.33", "version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.3.22", "version": "0.3.23",
"type": "module", "type": "module",
"description": "PyScript", "description": "PyScript",
"module": "./index.js", "module": "./index.js",
@@ -42,7 +42,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.6.16", "polyscript": "^0.6.18",
"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"

View File

@@ -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(

View File

@@ -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,7 +35,7 @@ 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) sig = inspect.signature(func)
# Function doesn't receive events # Function doesn't receive events
if not sig.parameters: if not sig.parameters:
@@ -35,11 +43,24 @@ def when(event_type=None, selector=None):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
func() 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):
try:
return func(*args, **kwargs)
except TypeError as e:
if "takes 0 positional arguments" in str(e):
return func()
raise
for el in elements: for el in elements:
add_event_listener(el, event_type, wrapper) 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

View File

@@ -0,0 +1 @@
from .pydom import dom as pydom

View File

@@ -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

View File

@@ -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>

View File

@@ -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"

View 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>

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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;
}; };