diff --git a/pyscript.core/src/stdlib/pyscript/__init__.py b/pyscript.core/src/stdlib/pyscript/__init__.py index 45d8e84f..dcd42f62 100644 --- a/pyscript.core/src/stdlib/pyscript/__init__.py +++ b/pyscript.core/src/stdlib/pyscript/__init__.py @@ -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( diff --git a/pyscript.core/src/stdlib/pyscript/event_handling.py b/pyscript.core/src/stdlib/pyscript/event_handling.py index 3fac89ea..23f930ce 100644 --- a/pyscript.core/src/stdlib/pyscript/event_handling.py +++ b/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 @@ -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 diff --git a/pyscript.core/src/stdlib/pyweb/__init__.py b/pyscript.core/src/stdlib/pyweb/__init__.py new file mode 100644 index 00000000..0a5b12ff --- /dev/null +++ b/pyscript.core/src/stdlib/pyweb/__init__.py @@ -0,0 +1 @@ +from .pydom import dom as pydom diff --git a/pyscript.core/src/stdlib/pyweb/pydom.py b/pyscript.core/src/stdlib/pyweb/pydom.py index 2eadcc81..a9110faa 100644 --- a/pyscript.core/src/stdlib/pyweb/pydom.py +++ b/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 @@ -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 diff --git a/pyscript.core/test/pydom.html b/pyscript.core/test/pydom.html index ff647381..cc959de4 100644 --- a/pyscript.core/test/pydom.html +++ b/pyscript.core/test/pydom.html @@ -3,7 +3,7 @@ - PyScript Next Plugin + PyDom Example diff --git a/pyscript.core/test/pydom.py b/pyscript.core/test/pydom.py index e251b8b4..ab8d0377 100644 --- a/pyscript.core/test/pydom.py +++ b/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" diff --git a/pyscript.core/test/pydom_mp.html b/pyscript.core/test/pydom_mp.html new file mode 100644 index 00000000..770e00b9 --- /dev/null +++ b/pyscript.core/test/pydom_mp.html @@ -0,0 +1,19 @@ + + + + + + PyDom Example (MicroPython) + + + + + + + + + + +
+ + diff --git a/pyscript.core/test/pyscript_dom/index.html b/pyscript.core/test/pyscript_dom/index.html index 63c88ee4..60e044f5 100644 --- a/pyscript.core/test/pyscript_dom/index.html +++ b/pyscript.core/test/pyscript_dom/index.html @@ -1,6 +1,6 @@ - PyperCard PyTest Suite + PyDom Test Suite @@ -32,7 +32,7 @@ - +

pyscript.dom Tests

You can pass test parameters to this test suite by passing them as query params on the url. diff --git a/pyscript.core/test/pyscript_dom/tests/test_dom.py b/pyscript.core/test/pyscript_dom/tests/test_dom.py index 3172e1d9..49c96a5a 100644 --- a/pyscript.core/test/pyscript_dom/tests/test_dom.py +++ b/pyscript.core/test/pyscript_dom/tests/test_dom.py @@ -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 diff --git a/pyscript.core/tests/integration/support.py b/pyscript.core/tests/integration/support.py index 4cc40db7..b9aadf83 100644 --- a/pyscript.core/tests/integration/support.py +++ b/pyscript.core/tests/integration/support.py @@ -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 diff --git a/pyscript.core/tests/integration/test_integration.py b/pyscript.core/tests/integration/test_integration.py new file mode 100644 index 00000000..4b9b0282 --- /dev/null +++ b/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 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 diff --git a/pyscript.core/types/stdlib/pyscript.d.ts b/pyscript.core/types/stdlib/pyscript.d.ts index 9805e008..6bff2b5e 100644 --- a/pyscript.core/types/stdlib/pyscript.d.ts +++ b/pyscript.core/types/stdlib/pyscript.d.ts @@ -7,6 +7,7 @@ declare namespace _default { "util.py": string; }; let pyweb: { + "__init__.py": string; "media.py": string; "pydom.py": string; };