From 4b90ebdef557452f27a7b7c3f9fb0a9f868db7e1 Mon Sep 17 00:00:00 2001 From: Andrea Giammarchi Date: Fri, 21 Jun 2024 14:49:20 +0200 Subject: [PATCH] Bring back pyweb as it was (#2105) --- pyscript.core/package-lock.json | 4 +- pyscript.core/package.json | 2 +- .../src/stdlib/pyscript/event_handling.py | 9 +- pyscript.core/src/stdlib/pyweb/__init__.py | 2 + pyscript.core/src/stdlib/pyweb/media.py | 95 ++ pyscript.core/src/stdlib/pyweb/pydom.py | 569 +++++++++++ pyscript.core/src/stdlib/pyweb/ui/__init__.py | 1 + pyscript.core/src/stdlib/pyweb/ui/elements.py | 947 ++++++++++++++++++ pyscript.core/test/pydom.py | 11 +- .../test/pyscript_dom/tests/test_dom.py | 119 ++- pyscript.core/tests/integration/test_pyweb.py | 7 +- pyscript.core/types/stdlib/pyscript.d.ts | 9 + 12 files changed, 1704 insertions(+), 71 deletions(-) create mode 100644 pyscript.core/src/stdlib/pyweb/__init__.py create mode 100644 pyscript.core/src/stdlib/pyweb/media.py create mode 100644 pyscript.core/src/stdlib/pyweb/pydom.py create mode 100644 pyscript.core/src/stdlib/pyweb/ui/__init__.py create mode 100644 pyscript.core/src/stdlib/pyweb/ui/elements.py diff --git a/pyscript.core/package-lock.json b/pyscript.core/package-lock.json index c6663652..472a2355 100644 --- a/pyscript.core/package-lock.json +++ b/pyscript.core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pyscript/core", - "version": "0.4.48", + "version": "0.4.50", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pyscript/core", - "version": "0.4.48", + "version": "0.4.50", "license": "APACHE-2.0", "dependencies": { "@ungap/with-resolvers": "^0.1.0", diff --git a/pyscript.core/package.json b/pyscript.core/package.json index 4889f05f..402f2771 100644 --- a/pyscript.core/package.json +++ b/pyscript.core/package.json @@ -1,6 +1,6 @@ { "name": "@pyscript/core", - "version": "0.4.48", + "version": "0.4.50", "type": "module", "description": "PyScript", "module": "./index.js", diff --git a/pyscript.core/src/stdlib/pyscript/event_handling.py b/pyscript.core/src/stdlib/pyscript/event_handling.py index 5e4438c6..6c233940 100644 --- a/pyscript.core/src/stdlib/pyscript/event_handling.py +++ b/pyscript.core/src/stdlib/pyscript/event_handling.py @@ -19,17 +19,16 @@ def when(event_type=None, selector=None): """ def decorator(func): - - from pyscript.web.elements import Element, ElementCollection - if isinstance(selector, str): elements = document.querySelectorAll(selector) else: # TODO: This is a hack that will be removed when pyscript becomes a package # and we can better manage the imports without circular dependencies - if isinstance(selector, Element): + from pyweb import pydom + + if isinstance(selector, pydom.Element): elements = [selector._js] - elif isinstance(selector, ElementCollection): + elif isinstance(selector, pydom.ElementCollection): elements = [el._js for el in selector] else: raise ValueError( diff --git a/pyscript.core/src/stdlib/pyweb/__init__.py b/pyscript.core/src/stdlib/pyweb/__init__.py new file mode 100644 index 00000000..80843cf2 --- /dev/null +++ b/pyscript.core/src/stdlib/pyweb/__init__.py @@ -0,0 +1,2 @@ +from .pydom import JSProperty +from .pydom import dom as pydom diff --git a/pyscript.core/src/stdlib/pyweb/media.py b/pyscript.core/src/stdlib/pyweb/media.py new file mode 100644 index 00000000..9f0ffcda --- /dev/null +++ b/pyscript.core/src/stdlib/pyweb/media.py @@ -0,0 +1,95 @@ +from pyodide.ffi import to_js +from pyscript import window + + +class Device: + """Device represents a media input or output device, such as a microphone, + camera, or headset. + """ + + def __init__(self, device): + self._js = device + + @property + def id(self): + return self._js.deviceId + + @property + def group(self): + return self._js.groupId + + @property + def kind(self): + return self._js.kind + + @property + def label(self): + return self._js.label + + def __getitem__(self, key): + return getattr(self, key) + + @classmethod + async def load(cls, audio=False, video=True): + """Load the device stream.""" + options = window.Object.new() + options.audio = audio + if isinstance(video, bool): + options.video = video + else: + # TODO: Think this can be simplified but need to check it on the pyodide side + + # TODO: this is pyodide specific. shouldn't be! + options.video = window.Object.new() + for k in video: + setattr( + options.video, + k, + to_js(video[k], dict_converter=window.Object.fromEntries), + ) + + stream = await window.navigator.mediaDevices.getUserMedia(options) + return stream + + async def get_stream(self): + key = self.kind.replace("input", "").replace("output", "") + options = {key: {"deviceId": {"exact": self.id}}} + + return await self.load(**options) + + +async def list_devices() -> list[dict]: + """ + Return the list of the currently available media input and output devices, + such as microphones, cameras, headsets, and so forth. + + Output: + + list(dict) - list of dictionaries representing the available media devices. + Each dictionary has the following keys: + * deviceId: a string that is an identifier for the represented device + that is persisted across sessions. It is un-guessable by other + applications and unique to the origin of the calling application. + It is reset when the user clears cookies (for Private Browsing, a + different identifier is used that is not persisted across sessions). + + * groupId: a string that is a group identifier. Two devices have the same + group identifier if they belong to the same physical device — for + example a monitor with both a built-in camera and a microphone. + + * kind: an enumerated value that is either "videoinput", "audioinput" + or "audiooutput". + + * label: a string describing this device (for example "External USB + Webcam"). + + Note: the returned list will omit any devices that are blocked by the document + Permission Policy: microphone, camera, speaker-selection (for output devices), + and so on. Access to particular non-default devices is also gated by the + Permissions API, and the list will omit devices for which the user has not + granted explicit permission. + """ + # https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices + return [ + Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices() + ] diff --git a/pyscript.core/src/stdlib/pyweb/pydom.py b/pyscript.core/src/stdlib/pyweb/pydom.py new file mode 100644 index 00000000..0ba41b25 --- /dev/null +++ b/pyscript.core/src/stdlib/pyweb/pydom.py @@ -0,0 +1,569 @@ +import inspect + +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 pyscript import display, document, window + +alert = window.alert + + +class JSProperty: + """JS property descriptor that directly maps to the property with the same + name in the underlying JS component.""" + + def __init__(self, name: str, allow_nones: bool = False): + self.name = name + self.allow_nones = allow_nones + + def __get__(self, obj, objtype=None): + return getattr(obj._js, self.name) + + def __set__(self, obj, value): + if not self.allow_nones and value is None: + return + setattr(obj._js, self.name, value) + + +class BaseElement: + def __init__(self, js_element): + self._js = js_element + self._parent = None + self.style = StyleProxy(self) + self._proxies = {} + + def __eq__(self, obj): + """Check if the element is the same as the other element by comparing + the underlying JS element""" + return isinstance(obj, BaseElement) and obj._js == self._js + + @property + def parent(self): + if self._parent: + return self._parent + + if self._js.parentElement: + self._parent = self.__class__(self._js.parentElement) + + return self._parent + + @property + def __class(self): + return self.__class__ if self.__class__ != PyDom else Element + + def create(self, type_, is_child=True, classes=None, html=None, label=None): + js_el = document.createElement(type_) + element = self.__class(js_el) + + if classes: + for class_ in classes: + element.add_class(class_) + + if html is not None: + element.html = html + + if label is not None: + element.label = label + + if is_child: + self.append(element) + + return element + + def find(self, selector): + """Return an ElementCollection representing all the child elements that + match the specified selector. + + Args: + selector (str): A string containing a selector expression + + Returns: + ElementCollection: A collection of elements matching the selector + """ + elements = self._js.querySelectorAll(selector) + if not elements: + return None + return ElementCollection([Element(el) for el in elements]) + + +class Element(BaseElement): + @property + def children(self): + return [self.__class__(el) for el in self._js.children] + + def append(self, child): + # TODO: this is Pyodide specific for now!!!!!! + # if we get passed a JSProxy Element directly we just map it to the + # higher level Python element + if inspect.isclass(JsProxy) and isinstance(child, JsProxy): + return self.append(Element(child)) + + elif isinstance(child, Element): + self._js.appendChild(child._js) + + return child + + elif isinstance(child, ElementCollection): + for el in child: + self.append(el) + + # -------- Pythonic Interface to Element -------- # + @property + def html(self): + return self._js.innerHTML + + @html.setter + def html(self, value): + self._js.innerHTML = value + + @property + def text(self): + return self._js.textContent + + @text.setter + def text(self, value): + self._js.textContent = value + + @property + def content(self): + # TODO: This breaks with with standard template elements. Define how to best + # handle this specifica use case. Just not support for now? + if self._js.tagName == "TEMPLATE": + warnings.warn( + "Content attribute not supported for template elements.", stacklevel=2 + ) + return None + return self._js.innerHTML + + @content.setter + def content(self, value): + # TODO: (same comment as above) + if self._js.tagName == "TEMPLATE": + warnings.warn( + "Content attribute not supported for template elements.", stacklevel=2 + ) + return + + display(value, target=self.id) + + @property + def id(self): + return self._js.id + + @id.setter + def id(self, value): + self._js.id = value + + @property + def options(self): + if "options" in self._proxies: + return self._proxies["options"] + + if not self._js.tagName.lower() in {"select", "datalist", "optgroup"}: + raise AttributeError( + f"Element {self._js.tagName} has no options attribute." + ) + self._proxies["options"] = OptionsProxy(self) + return self._proxies["options"] + + @property + def value(self): + return self._js.value + + @value.setter + def value(self, value): + # in order to avoid confusion to the user, we don't allow setting the + # value of elements that don't have a value attribute + if not hasattr(self._js, "value"): + raise AttributeError( + f"Element {self._js.tagName} has no value attribute. If you want to " + "force a value attribute, set it directly using the `_js.value = ` " + "javascript API attribute instead." + ) + self._js.value = value + + @property + def selected(self): + return self._js.selected + + @selected.setter + def selected(self, value): + # in order to avoid confusion to the user, we don't allow setting the + # value of elements that don't have a value attribute + if not hasattr(self._js, "selected"): + raise AttributeError( + f"Element {self._js.tagName} has no value attribute. If you want to " + "force a value attribute, set it directly using the `_js.value = ` " + "javascript API attribute instead." + ) + self._js.selected = value + + def clone(self, new_id=None): + clone = Element(self._js.cloneNode(True)) + clone.id = new_id + + return clone + + def remove_class(self, classname): + classList = self._js.classList + if isinstance(classname, list): + classList.remove(*classname) + else: + classList.remove(classname) + return self + + def add_class(self, classname): + classList = self._js.classList + if isinstance(classname, list): + classList.add(*classname) + else: + self._js.classList.add(classname) + return self + + @property + def classes(self): + classes = self._js.classList.values() + return [x for x in classes] + + def show_me(self): + self._js.scrollIntoView() + + def snap( + self, + to: BaseElement | str = None, + width: int | None = None, + height: int | None = None, + ): + """ + Captures a snapshot of a video element. (Only available for video elements) + + Inputs: + + * to: element where to save the snapshot of the video frame to + * width: width of the image + * height: height of the image + + Output: + (Element) canvas element where the video frame snapshot was drawn into + """ + if self._js.tagName != "VIDEO": + raise AttributeError("Snap method is only available for video Elements") + + if to is None: + canvas = self.create("canvas") + if width is None: + width = self._js.width + if height is None: + height = self._js.height + canvas._js.width = width + canvas._js.height = height + + elif isinstance(to, Element): + if to._js.tagName != "CANVAS": + raise TypeError("Element to snap to must a canvas.") + canvas = to + elif getattr(to, "tagName", "") == "CANVAS": + canvas = Element(to) + elif isinstance(to, str): + canvas = pydom[to][0] + if canvas._js.tagName != "CANVAS": + raise TypeError("Element to snap to must a be canvas.") + + canvas.draw(self, width, height) + + return canvas + + def download(self, filename: str = "snapped.png") -> None: + """Download the current element (only available for canvas elements) with the filename + provided in input. + + Inputs: + * filename (str): name of the file being downloaded + + Output: + None + """ + if self._js.tagName != "CANVAS": + raise AttributeError( + "The download method is only available for canvas Elements" + ) + + link = self.create("a") + link._js.download = filename + link._js.href = self._js.toDataURL() + link._js.click() + + def draw(self, what, width, height): + """Draw `what` on the current element (only available for canvas elements). + + Inputs: + + * what (canvas image source): An element to draw into the context. The specification permits any canvas + image source, specifically, an HTMLImageElement, an SVGImageElement, an HTMLVideoElement, + an HTMLCanvasElement, an ImageBitmap, an OffscreenCanvas, or a VideoFrame. + """ + if self._js.tagName != "CANVAS": + raise AttributeError( + "The draw method is only available for canvas Elements" + ) + + if isinstance(what, Element): + what = what._js + + # https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage + self._js.getContext("2d").drawImage(what, 0, 0, width, height) + + +class OptionsProxy: + """This class represents the options of a select element. It + allows to access to add and remove options by using the `add` and `remove` methods. + """ + + def __init__(self, element: Element) -> None: + self._element = element + if self._element._js.tagName.lower() != "select": + raise AttributeError( + f"Element {self._element._js.tagName} has no options attribute." + ) + + def add( + self, + value: Any = None, + html: str = None, + text: str = None, + before: Element | int = None, + **kws, + ) -> None: + """Add a new option to the select element""" + # create the option element and set the attributes + option = document.createElement("option") + if value is not None: + kws["value"] = value + if html is not None: + option.innerHTML = html + if text is not None: + kws["text"] = text + + for key, value in kws.items(): + option.setAttribute(key, value) + + if before: + if isinstance(before, Element): + before = before._js + + self._element._js.add(option, before) + + def remove(self, item: int) -> None: + """Remove the option at the specified index""" + self._element._js.remove(item) + + def clear(self) -> None: + """Remove all the options""" + for i in range(len(self)): + self.remove(0) + + @property + def options(self): + """Return the list of options""" + return [Element(opt) for opt in self._element._js.options] + + @property + def selected(self): + """Return the selected option""" + return self.options[self._element._js.selectedIndex] + + def __iter__(self): + yield from self.options + + def __len__(self): + return len(self.options) + + def __repr__(self): + return f"{self.__class__.__name__} (length: {len(self)}) {self.options}" + + def __getitem__(self, key): + return self.options[key] + + +class StyleProxy: # (dict): + def __init__(self, element: Element) -> None: + self._element = element + + @cached_property + def _style(self): + return self._element._js.style + + def __getitem__(self, key): + return self._style.getPropertyValue(key) + + def __setitem__(self, key, value): + self._style.setProperty(key, value) + + def remove(self, key): + self._style.removeProperty(key) + + def set(self, **kws): + for k, v in kws.items(): + self._element._js.style.setProperty(k, v) + + # CSS Properties + # Reference: https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L3799C1-L5005C2 + # Following prperties automatically generated from the above reference using + # tools/codegen_css_proxy.py + @property + def visible(self): + return self._element._js.style.visibility + + @visible.setter + def visible(self, value): + self._element._js.style.visibility = value + + +class StyleCollection: + def __init__(self, collection: "ElementCollection") -> None: + self._collection = collection + + def __get__(self, obj, objtype=None): + return obj._get_attribute("style") + + def __getitem__(self, key): + return self._collection._get_attribute("style")[key] + + def __setitem__(self, key, value): + for element in self._collection._elements: + element.style[key] = value + + def remove(self, key): + for element in self._collection._elements: + element.style.remove(key) + + +class ElementCollection: + def __init__(self, elements: [Element]) -> None: + self._elements = elements + self.style = StyleCollection(self) + + def __getitem__(self, key): + # If it's an integer we use it to access the elements in the collection + if isinstance(key, int): + return self._elements[key] + # If it's a slice we use it to support slice operations over the elements + # in the collection + elif isinstance(key, slice): + return ElementCollection(self._elements[key]) + + # If it's anything else (basically a string) we use it as a selector + # TODO: Write tests! + elements = self._element.querySelectorAll(key) + return ElementCollection([Element(el) for el in elements]) + + def __len__(self): + return len(self._elements) + + def __eq__(self, obj): + """Check if the element is the same as the other element by comparing + the underlying JS element""" + return isinstance(obj, ElementCollection) and obj._elements == self._elements + + def _get_attribute(self, attr, index=None): + if index is None: + return [getattr(el, attr) for el in self._elements] + + # As JQuery, when getting an attr, only return it for the first element + return getattr(self._elements[index], attr) + + def _set_attribute(self, attr, value): + for el in self._elements: + setattr(el, attr, value) + + @property + def html(self): + return self._get_attribute("html") + + @html.setter + def html(self, value): + self._set_attribute("html", value) + + @property + def value(self): + return self._get_attribute("value") + + @value.setter + def value(self, value): + self._set_attribute("value", value) + + @property + def children(self): + return self._elements + + def __iter__(self): + yield from self._elements + + def __repr__(self): + return f"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}" + + +class DomScope: + def __getattr__(self, __name: str): + element = document[f"#{__name}"] + if element: + return element[0] + + +class PyDom(BaseElement): + # Add objects we want to expose to the DOM namespace since this class instance is being + # remapped as "the module" itself + BaseElement = BaseElement + Element = Element + ElementCollection = ElementCollection + + def __init__(self): + # 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) + + def create(self, type_, classes=None, html=None): + return super().create(type_, is_child=False, classes=classes, html=html) + + def __getitem__(self, key): + elements = self._js.querySelectorAll(key) + if not elements: + return None + return ElementCollection([Element(el) for el in elements]) + + +dom = PyDom() diff --git a/pyscript.core/src/stdlib/pyweb/ui/__init__.py b/pyscript.core/src/stdlib/pyweb/ui/__init__.py new file mode 100644 index 00000000..a50ec40c --- /dev/null +++ b/pyscript.core/src/stdlib/pyweb/ui/__init__.py @@ -0,0 +1 @@ +from . import elements diff --git a/pyscript.core/src/stdlib/pyweb/ui/elements.py b/pyscript.core/src/stdlib/pyweb/ui/elements.py new file mode 100644 index 00000000..540e8f59 --- /dev/null +++ b/pyscript.core/src/stdlib/pyweb/ui/elements.py @@ -0,0 +1,947 @@ +import inspect +import sys + +from pyscript import document, when, window +from pyweb import JSProperty, pydom + +#: A flag to show if MicroPython is the current Python interpreter. +is_micropython = "MicroPython" in sys.version + + +def getmembers_static(cls): + """Cross-interpreter implementation of inspect.getmembers_static.""" + + if is_micropython: # pragma: no cover + return [(name, getattr(cls, name)) for name, _ in inspect.getmembers(cls)] + + return inspect.getmembers_static(cls) + + +class ElementBase(pydom.Element): + tag = "div" + + # GLOBAL ATTRIBUTES + # These are attribute that all elements have (this list is a subset of the official one) + # We are trying to capture the most used ones + accesskey = JSProperty("accesskey") + autofocus = JSProperty("autofocus") + autocapitalize = JSProperty("autocapitalize") + className = JSProperty("className") + contenteditable = JSProperty("contenteditable") + draggable = JSProperty("draggable") + enterkeyhint = JSProperty("enterkeyhint") + hidden = JSProperty("hidden") + id = JSProperty("id") + lang = JSProperty("lang") + nonce = JSProperty("nonce") + part = JSProperty("part") + popover = JSProperty("popover") + slot = JSProperty("slot") + spellcheck = JSProperty("spellcheck") + tabindex = JSProperty("tabindex") + title = JSProperty("title") + translate = JSProperty("translate") + virtualkeyboardpolicy = JSProperty("virtualkeyboardpolicy") + + def __init__(self, style=None, **kwargs): + super().__init__(document.createElement(self.tag)) + + # set all the style properties provided in input + if isinstance(style, dict): + for key, value in style.items(): + self.style[key] = value + elif style is None: + pass + else: + raise ValueError( + f"Style should be a dictionary, received {style} (type {type(style)}) instead." + ) + + # IMPORTANT!!! This is used to auto-harvest all input arguments and set them as properties + self._init_properties(**kwargs) + + def _init_properties(self, **kwargs): + """Set all the properties (of type JSProperties) provided in input as properties + of the class instance. + + Args: + **kwargs: The properties to set + """ + # Look at all the properties of the class and see if they were provided in kwargs + for attr_name, attr in getmembers_static(self.__class__): + # For each one, actually check if it is a property of the class and set it + if isinstance(attr, JSProperty) and attr_name in kwargs: + try: + setattr(self, attr_name, kwargs[attr_name]) + except Exception as e: + print(f"Error setting {attr_name} to {kwargs[attr_name]}: {e}") + raise + + +class TextElementBase(ElementBase): + def __init__(self, content=None, style=None, **kwargs): + super().__init__(style=style, **kwargs) + + # If it's an element, append the element + if isinstance(content, pydom.Element): + self.append(content) + # If it's a list of elements + elif isinstance(content, list): + for item in content: + self.append(item) + # If the content wasn't set just ignore + elif content is None: + pass + else: + # Otherwise, set content as the html of the element + self.html = content + + +# IMPORTANT: For all HTML components defined below, we are not mapping all +# available attributes, just the global and the most common ones. +# If you need to access a specific attribute, you can always use the `_js.` +class a(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a""" + + tag = "a" + + download = JSProperty("download") + href = JSProperty("href") + referrerpolicy = JSProperty("referrerpolicy") + rel = JSProperty("rel") + target = JSProperty("target") + type = JSProperty("type") + + +class abbr(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/abbr""" + + tag = "abbr" + + +class address(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address""" + + tag = "address" + + +class area(ElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area""" + + tag = "area" + + alt = JSProperty("alt") + coords = JSProperty("coords") + download = JSProperty("download") + href = JSProperty("href") + ping = JSProperty("ping") + referrerpolicy = JSProperty("referrerpolicy") + rel = JSProperty("rel") + shape = JSProperty("shape") + target = JSProperty("target") + + +class article(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article""" + + tag = "article" + + +class aside(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/aside""" + + tag = "aside" + + +class audio(ElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio""" + + tag = "audio" + + autoplay = JSProperty("autoplay") + controls = JSProperty("controls") + controlslist = JSProperty("controlslist") + crossorigin = JSProperty("crossorigin") + disableremoteplayback = JSProperty("disableremoteplayback") + loop = JSProperty("loop") + muted = JSProperty("muted") + preload = JSProperty("preload") + src = JSProperty("src") + + +class b(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b""" + + tag = "b" + + +class blockquote(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote""" + + tag = "blockquote" + + cite = JSProperty("cite") + + +class br(ElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br""" + + tag = "br" + + +class button(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button""" + + tag = "button" + + autofocus = JSProperty("autofocus") + disabled = JSProperty("disabled") + form = JSProperty("form") + formaction = JSProperty("formaction") + formenctype = JSProperty("formenctype") + formmethod = JSProperty("formmethod") + formnovalidate = JSProperty("formnovalidate") + formtarget = JSProperty("formtarget") + name = JSProperty("name") + type = JSProperty("type") + value = JSProperty("value") + + +class canvas(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas""" + + tag = "canvas" + + height = JSProperty("height") + width = JSProperty("width") + + +class caption(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption""" + + tag = "caption" + + +class cite(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/cite""" + + tag = "cite" + + +class code(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code""" + + tag = "code" + + +class data(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data""" + + tag = "data" + + value = JSProperty("value") + + +class datalist(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist""" + + tag = "datalist" + + +class dd(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dd""" + + tag = "dd" + + +class del_(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del""" + + tag = "del" + + cite = JSProperty("cite") + datetime = JSProperty("datetime") + + +class details(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details""" + + tag = "details" + + open = JSProperty("open") + + +class dialog(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog""" + + tag = "dialog" + + open = JSProperty("open") + + +class div(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div""" + + tag = "div" + + +class dl(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl""" + + tag = "dl" + + value = JSProperty("value") + + +class dt(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt""" + + tag = "dt" + + +class em(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em""" + + tag = "em" + + +class embed(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed""" + + tag = "embed" + + height = JSProperty("height") + src = JSProperty("src") + type = JSProperty("type") + width = JSProperty("width") + + +class fieldset(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset""" + + tag = "fieldset" + + disabled = JSProperty("disabled") + form = JSProperty("form") + name = JSProperty("name") + + +class figcaption(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption""" + + tag = "figcaption" + + +class figure(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure""" + + tag = "figure" + + +class footer(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/footer""" + + tag = "footer" + + +class form(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form""" + + tag = "form" + + accept_charset = JSProperty("accept-charset") + action = JSProperty("action") + autocapitalize = JSProperty("autocapitalize") + autocomplete = JSProperty("autocomplete") + enctype = JSProperty("enctype") + name = JSProperty("name") + method = JSProperty("method") + nonvalidate = JSProperty("nonvalidate") + rel = JSProperty("rel") + target = JSProperty("target") + + +class h1(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1""" + + tag = "h1" + + +class h2(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h2""" + + tag = "h2" + + +class h3(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h3""" + + tag = "h3" + + +class h4(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h4""" + + tag = "h4" + + +class h5(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h5""" + + tag = "h5" + + +class h6(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h6""" + + tag = "h6" + + +class header(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header""" + + tag = "header" + + +class hgroup(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup""" + + tag = "hgroup" + + +class hr(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr""" + + tag = "hr" + + +class i(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i""" + + tag = "i" + + +class iframe(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe""" + + tag = "iframe" + + allow = JSProperty("allow") + allowfullscreen = JSProperty("allowfullscreen") + height = JSProperty("height") + loading = JSProperty("loading") + name = JSProperty("name") + referrerpolicy = JSProperty("referrerpolicy") + sandbox = JSProperty("sandbox") + src = JSProperty("src") + srcdoc = JSProperty("srcdoc") + width = JSProperty("width") + + +class img(ElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img""" + + tag = "img" + + alt = JSProperty("alt") + crossorigin = JSProperty("crossorigin") + decoding = JSProperty("decoding") + fetchpriority = JSProperty("fetchpriority") + height = JSProperty("height") + ismap = JSProperty("ismap") + loading = JSProperty("loading") + referrerpolicy = JSProperty("referrerpolicy") + sizes = JSProperty("sizes") + src = JSProperty("src") + width = JSProperty("width") + + +# NOTE: Input is a reserved keyword in Python, so we use input_ instead +class input_(ElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input""" + + tag = "input" + + accept = JSProperty("accept") + alt = JSProperty("alt") + autofocus = JSProperty("autofocus") + capture = JSProperty("capture") + checked = JSProperty("checked") + dirname = JSProperty("dirname") + disabled = JSProperty("disabled") + form = JSProperty("form") + formaction = JSProperty("formaction") + formenctype = JSProperty("formenctype") + formmethod = JSProperty("formmethod") + formnovalidate = JSProperty("formnovalidate") + formtarget = JSProperty("formtarget") + height = JSProperty("height") + list = JSProperty("list") + max = JSProperty("max") + maxlength = JSProperty("maxlength") + min = JSProperty("min") + minlength = JSProperty("minlength") + multiple = JSProperty("multiple") + name = JSProperty("name") + pattern = JSProperty("pattern") + placeholder = JSProperty("placeholder") + popovertarget = JSProperty("popovertarget") + popovertargetaction = JSProperty("popovertargetaction") + readonly = JSProperty("readonly") + required = JSProperty("required") + size = JSProperty("size") + src = JSProperty("src") + step = JSProperty("step") + type = JSProperty("type") + value = JSProperty("value") + width = JSProperty("width") + + +class ins(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins""" + + tag = "ins" + + cite = JSProperty("cite") + datetime = JSProperty("datetime") + + +class kbd(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd""" + + tag = "kbd" + + +class label(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label""" + + tag = "label" + + for_ = JSProperty("for") + + +class legend(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend""" + + tag = "legend" + + +class li(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li""" + + tag = "li" + + value = JSProperty("value") + + +class link(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link""" + + tag = "link" + + as_ = JSProperty("as") + crossorigin = JSProperty("crossorigin") + disabled = JSProperty("disabled") + fetchpriority = JSProperty("fetchpriority") + href = JSProperty("href") + imagesizes = JSProperty("imagesizes") + imagesrcset = JSProperty("imagesrcset") + integrity = JSProperty("integrity") + media = JSProperty("media") + rel = JSProperty("rel") + referrerpolicy = JSProperty("referrerpolicy") + sizes = JSProperty("sizes") + title = JSProperty("title") + type = JSProperty("type") + + +class main(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/main""" + + tag = "main" + + +class map_(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map""" + + tag = "map" + + name = JSProperty("name") + + +class mark(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark""" + + tag = "mark" + + +class menu(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu""" + + tag = "menu" + + +class meter(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter""" + + tag = "meter" + + form = JSProperty("form") + high = JSProperty("high") + low = JSProperty("low") + max = JSProperty("max") + min = JSProperty("min") + optimum = JSProperty("optimum") + value = JSProperty("value") + + +class nav(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav""" + + tag = "nav" + + +class object_(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object""" + + tag = "object" + + data = JSProperty("data") + form = JSProperty("form") + height = JSProperty("height") + name = JSProperty("name") + type = JSProperty("type") + usemap = JSProperty("usemap") + width = JSProperty("width") + + +class ol(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol""" + + tag = "ol" + + reversed = JSProperty("reversed") + start = JSProperty("start") + type = JSProperty("type") + + +class optgroup(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup""" + + tag = "optgroup" + + disabled = JSProperty("disabled") + label = JSProperty("label") + + +class option(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option""" + + tag = "option" + + disabled = JSProperty("value") + label = JSProperty("label") + selected = JSProperty("selected") + value = JSProperty("value") + + +class output(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output""" + + tag = "output" + + for_ = JSProperty("for") + form = JSProperty("form") + name = JSProperty("name") + + +class p(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p""" + + tag = "p" + + +class picture(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture""" + + tag = "picture" + + +class pre(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre""" + + tag = "pre" + + +class progress(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress""" + + tag = "progress" + + max = JSProperty("max") + value = JSProperty("value") + + +class q(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q""" + + tag = "q" + + cite = JSProperty("cite") + + +class s(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/s""" + + tag = "s" + + +class script(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script""" + + tag = "script" + + # Let's add async manually since it's a reserved keyword in Python + async_ = JSProperty("async") + blocking = JSProperty("blocking") + crossorigin = JSProperty("crossorigin") + defer = JSProperty("defer") + fetchpriority = JSProperty("fetchpriority") + integrity = JSProperty("integrity") + nomodule = JSProperty("nomodule") + nonce = JSProperty("nonce") + referrerpolicy = JSProperty("referrerpolicy") + src = JSProperty("src") + type = JSProperty("type") + + +class section(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section""" + + tag = "section" + + +class select(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select""" + + tag = "select" + + +class small(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/small""" + + tag = "small" + + +class source(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source""" + + tag = "source" + + media = JSProperty("media") + sizes = JSProperty("sizes") + src = JSProperty("src") + srcset = JSProperty("srcset") + type = JSProperty("type") + + +class span(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span""" + + tag = "span" + + +class strong(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong""" + + tag = "strong" + + +class style(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style""" + + tag = "style" + + blocking = JSProperty("blocking") + media = JSProperty("media") + nonce = JSProperty("nonce") + title = JSProperty("title") + + +class sub(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub""" + + tag = "sub" + + +class summary(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary""" + + tag = "summary" + + +class sup(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup""" + + tag = "sup" + + +class table(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table""" + + tag = "table" + + +class tbody(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody""" + + tag = "tbody" + + +class td(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td""" + + tag = "td" + + colspan = JSProperty("colspan") + headers = JSProperty("headers") + rowspan = JSProperty("rowspan") + + +class template(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template""" + + tag = "template" + + shadowrootmode = JSProperty("shadowrootmode") + + +class textarea(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea""" + + tag = "textarea" + + autocapitalize = JSProperty("autocapitalize") + autocomplete = JSProperty("autocomplete") + autofocus = JSProperty("autofocus") + cols = JSProperty("cols") + dirname = JSProperty("dirname") + disabled = JSProperty("disabled") + form = JSProperty("form") + maxlength = JSProperty("maxlength") + minlength = JSProperty("minlength") + name = JSProperty("name") + placeholder = JSProperty("placeholder") + readonly = JSProperty("readonly") + required = JSProperty("required") + rows = JSProperty("rows") + spellcheck = JSProperty("spellcheck") + wrap = JSProperty("wrap") + + +class tfoot(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tfoot""" + + tag = "tfoot" + + +class th(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th""" + + tag = "th" + + +class thead(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead""" + + tag = "thead" + + +class time(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time""" + + tag = "time" + + datetime = JSProperty("datetime") + + +class title(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title""" + + tag = "title" + + +class tr(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr""" + + tag = "tr" + + abbr = JSProperty("abbr") + colspan = JSProperty("colspan") + headers = JSProperty("headers") + rowspan = JSProperty("rowspan") + scope = JSProperty("scope") + + +class track(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track""" + + tag = "track" + + default = JSProperty("default") + kind = JSProperty("kind") + label = JSProperty("label") + src = JSProperty("src") + srclang = JSProperty("srclang") + + +class u(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/u""" + + tag = "u" + + +class ul(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul""" + + tag = "ul" + + +class var(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/var""" + + tag = "var" + + +class video(TextElementBase): + """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video""" + + tag = "video" + + autoplay = JSProperty("autoplay") + controls = JSProperty("controls") + crossorigin = JSProperty("crossorigin") + disablepictureinpicture = JSProperty("disablepictureinpicture") + disableremoteplayback = JSProperty("disableremoteplayback") + height = JSProperty("height") + loop = JSProperty("loop") + muted = JSProperty("muted") + playsinline = JSProperty("playsinline") + poster = JSProperty("poster") + preload = JSProperty("preload") + src = JSProperty("src") + width = JSProperty("width") + + +# Custom Elements +class grid(TextElementBase): + tag = "div" + + def __init__(self, layout, content=None, gap=None, **kwargs): + super().__init__(content, **kwargs) + self.style["display"] = "grid" + self.style["grid-template-columns"] = layout + + # TODO: This should be a property + if not gap is None: + self.style["gap"] = gap diff --git a/pyscript.core/test/pydom.py b/pyscript.core/test/pydom.py index 3f579e8e..2f134688 100644 --- a/pyscript.core/test/pydom.py +++ b/pyscript.core/test/pydom.py @@ -4,7 +4,7 @@ import time from datetime import datetime as dt from pyscript import display, when -from pyscript.web import dom +from pyweb import pydom display(sys.version, target="system-info") @@ -19,15 +19,18 @@ def on_click(): 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") + display(f"Hello from PyScript, time is: {timenow}", append=False, target="result") @when("click", "#color-button") def on_color_click(event): - btn = dom["#result"] + btn = pydom["#result"] btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}" @when("click", "#color-reset-button") def reset_color(*args, **kwargs): - dom["#result"].style["background-color"] = "white" + pydom["#result"].style["background-color"] = "white" + + +# btn_reset = pydom["#color-reset-button"][0].when('click', reset_color) diff --git a/pyscript.core/test/pyscript_dom/tests/test_dom.py b/pyscript.core/test/pyscript_dom/tests/test_dom.py index 7f4af822..d3a23c80 100644 --- a/pyscript.core/test/pyscript_dom/tests/test_dom.py +++ b/pyscript.core/test/pyscript_dom/tests/test_dom.py @@ -2,13 +2,22 @@ from unittest import mock import pytest from pyscript import document, when -from pyscript.web import dom -from pyscript.web import elements as el +from pyweb import pydom class TestDocument: def test__element(self): - assert dom._js == document + assert pydom._js == document + + def test_no_parent(self): + assert pydom.parent is None + + def test_create_element(self): + new_el = pydom.create("div") + assert isinstance(new_el, pydom.BaseElement) + assert new_el._js.tagName == "DIV" + # EXPECT the new element to be associated with the document + assert new_el.parent == None def test_getitem_by_id(): @@ -17,14 +26,14 @@ def test_getitem_by_id(): txt = "You found test_id_selector" selector = f"#{id_}" # EXPECT the element to be found by id - result = dom[selector] + result = pydom[selector] div = result[0] # EXPECT the element text value to match what we expect and what # the JS document.querySelector API would return assert document.querySelector(selector).innerHTML == div.html == txt # EXPECT the results to be of the right types - assert isinstance(div, el.BaseElement) - assert isinstance(result, dom.ElementCollection) + assert isinstance(div, pydom.BaseElement) + assert isinstance(result, pydom.ElementCollection) def test_getitem_by_class(): @@ -34,7 +43,7 @@ def test_getitem_by_class(): "test_selector_w_children_child_1", ] expected_class = "a-test-class" - result = dom[f".{expected_class}"] + result = pydom[f".{expected_class}"] div = result[0] # EXPECT to find exact number of elements with the class in the page (== 3) @@ -45,7 +54,7 @@ def test_getitem_by_class(): def test_read_n_write_collection_elements(): - elements = dom[".multi-elems"] + elements = pydom[".multi-elems"] for element in elements: assert element.html == f"Content {element.id.replace('#', '')}" @@ -60,15 +69,15 @@ class TestElement: def test_query(self): # GIVEN an existing element on the page, with at least 1 child element id_ = "test_selector_w_children" - parent_div = dom[f"#{id_}"][0] + parent_div = pydom[f"#{id_}"][0] # EXPECT it to be able to query for the first child element div = parent_div.find("div")[0] # EXPECT the new element to be associated with the parent assert div.parent == parent_div - # EXPECT the new element to be a el.BaseElement - assert isinstance(div, el.BaseElement) + # EXPECT the new element to be a BaseElement + assert isinstance(div, pydom.BaseElement) # EXPECT the div attributes to be == to how they are configured in the page assert div.html == "Child 1" assert div.id == "test_selector_w_children_child_1" @@ -77,8 +86,8 @@ class TestElement: # GIVEN 2 different Elements pointing to the same underlying element id_ = "test_id_selector" selector = f"#{id_}" - div = dom[selector][0] - div2 = dom[selector][0] + div = pydom[selector][0] + div2 = pydom[selector][0] # EXPECT them to be equal assert div == div2 @@ -93,27 +102,27 @@ class TestElement: def test_append_element(self): id_ = "element-append-tests" - div = dom[f"#{id_}"][0] + div = pydom[f"#{id_}"][0] len_children_before = len(div.children) - new_el = el.p("new element") + new_el = div.create("p") div.append(new_el) assert len(div.children) == len_children_before + 1 assert div.children[-1] == new_el def test_append_js_element(self): id_ = "element-append-tests" - div = dom[f"#{id_}"][0] + div = pydom[f"#{id_}"][0] len_children_before = len(div.children) - new_el = el.p("new element") + new_el = div.create("p") div.append(new_el._js) assert len(div.children) == len_children_before + 1 assert div.children[-1] == new_el def test_append_collection(self): id_ = "element-append-tests" - div = dom[f"#{id_}"][0] + div = pydom[f"#{id_}"][0] len_children_before = len(div.children) - collection = dom[".collection"] + collection = pydom[".collection"] div.append(collection) assert len(div.children) == len_children_before + len(collection) @@ -123,16 +132,16 @@ class TestElement: def test_read_classes(self): id_ = "test_class_selector" expected_class = "a-test-class" - div = dom[f"#{id_}"][0] + div = pydom[f"#{id_}"][0] assert div.classes == [expected_class] def test_add_remove_class(self): id_ = "div-no-classes" classname = "tester-class" - div = dom[f"#{id_}"][0] + div = pydom[f"#{id_}"][0] assert not div.classes div.add_class(classname) - same_div = dom[f"#{id_}"][0] + same_div = pydom[f"#{id_}"][0] assert div.classes == [classname] == same_div.classes div.remove_class(classname) assert div.classes == [] == same_div.classes @@ -140,7 +149,7 @@ class TestElement: def test_when_decorator(self): called = False - just_a_button = dom["#a-test-button"][0] + just_a_button = pydom["#a-test-button"][0] @when("click", just_a_button) def on_click(event): @@ -148,7 +157,7 @@ class TestElement: called = True # Now let's simulate a click on the button (using the low level JS API) - # so we don't risk dom getting in the way + # so we don't risk pydom getting in the way assert not called just_a_button._js.click() @@ -156,7 +165,7 @@ class TestElement: def test_html_attribute(self): # GIVEN an existing element on the page with a known empty text content - div = dom["#element_attribute_tests"][0] + div = pydom["#element_attribute_tests"][0] # WHEN we set the html attribute div.html = "New Content" @@ -168,7 +177,7 @@ class TestElement: def test_text_attribute(self): # GIVEN an existing element on the page with a known empty text content - div = dom["#element_attribute_tests"][0] + div = pydom["#element_attribute_tests"][0] # WHEN we set the html attribute div.text = "New Content" @@ -181,12 +190,12 @@ class TestElement: class TestCollection: def test_iter_eq_children(self): - elements = dom[".multi-elems"] + elements = pydom[".multi-elems"] assert [el for el in elements] == [el for el in elements.children] assert len(elements) == 3 def test_slices(self): - elements = dom[".multi-elems"] + elements = pydom[".multi-elems"] assert elements[0] _slice = elements[:2] assert len(_slice) == 2 @@ -196,26 +205,26 @@ class TestCollection: def test_style_rule(self): selector = ".multi-elems" - elements = dom[selector] + elements = pydom[selector] for el in elements: assert el.style["background-color"] != "red" elements.style["background-color"] = "red" - for i, el in enumerate(dom[selector]): + for i, el in enumerate(pydom[selector]): assert elements[i].style["background-color"] == "red" assert el.style["background-color"] == "red" elements.style.remove("background-color") - for i, el in enumerate(dom[selector]): + for i, el in enumerate(pydom[selector]): assert el.style["background-color"] != "red" assert elements[i].style["background-color"] != "red" def test_when_decorator(self): called = False - buttons_collection = dom["button"] + buttons_collection = pydom["button"] @when("click", buttons_collection) def on_click(event): @@ -223,7 +232,7 @@ class TestCollection: called = True # Now let's simulate a click on the button (using the low level JS API) - # so we don't risk dom getting in the way + # so we don't risk pydom getting in the way assert not called for button in buttons_collection: button._js.click() @@ -233,32 +242,32 @@ class TestCollection: class TestCreation: def test_create_document_element(self): - # TODO: This test should probably be removed since it's testing the elements module - new_el = el.div("new element") + new_el = pydom.create("div") new_el.id = "new_el_id" - assert isinstance(new_el, el.BaseElement) + assert isinstance(new_el, pydom.BaseElement) assert new_el._js.tagName == "DIV" # EXPECT the new element to be associated with the document assert new_el.parent == None - dom.body.append(new_el) + pydom.body.append(new_el) - assert dom["#new_el_id"][0].parent == dom.body + assert pydom["#new_el_id"][0].parent == pydom.body def test_create_element_child(self): selector = "#element-creation-test" - parent_div = dom[selector][0] + parent_div = pydom[selector][0] # Creating an element from another element automatically creates that element # as a child of the original element - new_el = el.p("a div", classes=["code-description"], html="Ciao PyScripters!") - parent_div.append(new_el) + new_el = parent_div.create( + "p", classes=["code-description"], html="Ciao PyScripters!" + ) - assert isinstance(new_el, el.BaseElement) + assert isinstance(new_el, pydom.BaseElement) assert new_el._js.tagName == "P" - # EXPECT the new element to be associated with the document assert new_el.parent == parent_div - assert dom[selector][0].children[0] == new_el + + assert pydom[selector][0].children[0] == new_el class TestInput: @@ -272,7 +281,7 @@ class TestInput: def test_value(self): for id_ in self.input_ids: expected_type = id_.split("_")[-1] - result = dom[f"#{id_}"] + result = pydom[f"#{id_}"] input_el = result[0] assert input_el._js.type == expected_type assert input_el.value == f"Content {id_}" == input_el._js.value @@ -290,7 +299,7 @@ class TestInput: def test_set_value_collection(self): for id_ in self.input_ids: - input_el = dom[f"#{id_}"] + input_el = pydom[f"#{id_}"] assert input_el.value[0] == f"Content {id_}" == input_el[0].value @@ -299,35 +308,35 @@ class TestInput: assert input_el.value[0] == new_value == input_el[0].value def test_element_without_value(self): - result = dom[f"#tests-terminal"][0] + result = pydom[f"#tests-terminal"][0] with pytest.raises(AttributeError): result.value = "some value" def test_element_without_collection(self): - result = dom[f"#tests-terminal"] + result = pydom[f"#tests-terminal"] with pytest.raises(AttributeError): result.value = "some value" def test_element_without_collection(self): - result = dom[f"#tests-terminal"] + result = pydom[f"#tests-terminal"] with pytest.raises(AttributeError): result.value = "some value" class TestSelect: def test_select_options_iter(self): - select = dom[f"#test_select_element_w_options"][0] + select = pydom[f"#test_select_element_w_options"][0] for i, option in enumerate(select.options, 1): assert option.value == f"{i}" assert option.html == f"Option {i}" def test_select_options_len(self): - select = dom[f"#test_select_element_w_options"][0] + select = pydom[f"#test_select_element_w_options"][0] assert len(select.options) == 2 def test_select_options_clear(self): - select = dom[f"#test_select_element_to_clear"][0] + select = pydom[f"#test_select_element_to_clear"][0] assert len(select.options) == 3 select.options.clear() @@ -336,7 +345,7 @@ class TestSelect: def test_select_element_add(self): # GIVEN the existing select element with no options - select = dom[f"#test_select_element"][0] + select = pydom[f"#test_select_element"][0] # EXPECT the select element to have no options assert len(select.options) == 0 @@ -417,7 +426,7 @@ class TestSelect: def test_select_options_remove(self): # GIVEN the existing select element with 3 options - select = dom[f"#test_select_element_to_remove"][0] + select = pydom[f"#test_select_element_to_remove"][0] # EXPECT the select element to have 3 options assert len(select.options) == 4 @@ -439,7 +448,7 @@ class TestSelect: def test_select_get_selected_option(self): # GIVEN the existing select element with one selected option - select = dom[f"#test_select_element_w_options"][0] + select = pydom[f"#test_select_element_w_options"][0] # WHEN we get the selected option selected_option = select.options.selected diff --git a/pyscript.core/tests/integration/test_pyweb.py b/pyscript.core/tests/integration/test_pyweb.py index 80b2567d..d83424c8 100644 --- a/pyscript.core/tests/integration/test_pyweb.py +++ b/pyscript.core/tests/integration/test_pyweb.py @@ -101,11 +101,10 @@ class TestElements(PyScriptTest): code_ = f""" from pyscript import when """ self.pyscript_run(code_) diff --git a/pyscript.core/types/stdlib/pyscript.d.ts b/pyscript.core/types/stdlib/pyscript.d.ts index fb69b323..8dd1c7c3 100644 --- a/pyscript.core/types/stdlib/pyscript.d.ts +++ b/pyscript.core/types/stdlib/pyscript.d.ts @@ -17,5 +17,14 @@ declare namespace _default { }; "websocket.py": string; }; + let pyweb: { + "__init__.py": string; + "media.py": string; + "pydom.py": string; + ui: { + "__init__.py": string; + "elements.py": string; + }; + }; } export default _default;