Cleanup pyscript web elements (#2094)

* change pydom example to use new pyscript.web namespace

* change tests to use new pyscript.web namespace

* create new pyscript.web package and move pydom to pyscript.web.dom

* add __init__ to pyscript.web and expose the dom instance instead of the pyscript.web.dom module

* move elements from pyweb.ui to pyscript.web and temp fix pydom import

* moved of elements file completed

* moved media from pyweb to pyscript.web

* RIP pyweb

* move JSProperty from pyscript.web.dom to pyscript.web.elements

* move element classes from pyscript.web.dom to pyscript.web.elements

* first round of fixes while running tests

* fix test typo

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* restore right type type returned for Element.parent. ALL TESTS PASS LOCALLY NOW

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* clean up dom.py from dead commented code and osbolete comments

* bugfix: dom shouldn't return None when it can't find any element for a specific selector so it now returns an empty collection

* additional cleanup in tests

* lint

* initial cleaning up of unused modules

* change element.append to not consider types anymore and add tests for appending elements.Element or a JsProxy object

* add Element.append tests for append JS elements directly and appending nodeList as well

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tag and create the correct subclass of Element.

* Move: Element.snap -> video.snap

* Move: Element.download and draw to canvas.download and draw.

* Minor cleanups.

* Commenting.

* Allow css classes to be passed to Element constructor.

* Commenting.

* Typo fix.

* Make html, id and text JSProperties.

* Commenting.

* Remove unnecessary selected attribute on BaseElement.

* Extract: BaseElement.from_js -> element_from_js

* Pass *args and **kwargs to element_from_js and remove BaseElement.create

* Move value attribute to specific Element subclasses.

* fix: wrapping of existing js elements.

* Add body and head elements so parent and children work everywhere.

* Revert order of HasOptions mixin for the select element.

* Comment out tests that are no longer relevant (see comment).

* Use correct super args in mixin.

* Have to use element_from_js when returning options from OptionsProxy.

* rename: StyleProxy -> Style, OptionsProxy -> Options and added Classes.

* Remove cached_property.

* Remove list-y methods from Classes collection.

* Allow explicit children or *args for containers.

* controversial: fix tests to use find rather than dom

* Add html element so (say) body.parent does what is expected.

* Collapse Element class hierarchy.

* rename: js_element -> dom_element

* rename: element_from_js -> element_from_dom

* replace: JS with DOM

* rename: _js -> _dom_element

* fix dom tests.

* Complete element list with void elements derived from Element.

* Added attributes to the newly added Element subclasses.

* remove dom module, replace with instance.

Also, remove media :)

* fix: typo in test for 'b' element.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Remove dom and media modules.

* fix up ts definitions.

* Added missing import (used in content property).

* Added TODO :)

* wip: Add ClassesCollection

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Attempt to ask black to leave class list alone.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add classes attribute to ElementCollection

* wip: work on classes collection

* Extract code to get set of all class names in ClassesCollection.

* Update elements.py

* Polishing.

* Make children return an ElementCollection

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* wip: Add the ability to set multiple properties.

* Add __getitem__ back to the dom object.

* Put validation when setting DOM properties back in.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* All tests green.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Remove unnecessary comment.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Martin <martin.chilvers@gmail.com>
This commit is contained in:
Fabio Pliger
2024-07-03 17:21:23 -04:00
committed by GitHub
parent 67d47511d5
commit f8f7ba89c1
14 changed files with 1270 additions and 2614 deletions

View File

@@ -19,22 +19,23 @@ 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)
# TODO: This is a hack that will be removed when pyscript becomes a package
# and we can better manage the imports without circular dependencies
elif isinstance(selector, Element):
elements = [selector._dom_element]
elif isinstance(selector, ElementCollection):
elements = [el._dom_element for el in 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
from pyweb import pydom
if isinstance(selector, pydom.Element):
elements = [selector._js]
elif isinstance(selector, pydom.ElementCollection):
elements = [el._js for el in selector]
if isinstance(selector, list):
elements = selector
else:
raise ValueError(
f"Invalid selector: {selector}. Selector must"
" be a string, a pydom.Element or a pydom.ElementCollection."
)
elements = [selector]
try:
sig = inspect.signature(func)
# Function doesn't receive events

View File

@@ -1,5 +1,19 @@
from . import elements
from pyscript import document
from pyscript.web.elements import ElementCollection, element_from_dom
# Ugly trick to hide the dom module in the web package since we want the module
# to allow querying right away.
from .dom import dom
class DOM:
def __init__(self):
self.body = element_from_dom(document.body)
self.head = element_from_dom(document.head)
def __getitem__(self, selector):
return self.find(selector)
def find(self, selector):
return ElementCollection(
[element_from_dom(el) for el in document.querySelectorAll(selector)]
)
dom = DOM()

View File

@@ -1,21 +0,0 @@
from pyscript import document
from pyscript.web.elements import Element, ElementCollection
class PyDom:
# Add objects we want to expose to the DOM namespace since this class instance is being
# remapped as "the module" itself
ElementCollection = ElementCollection
def __init__(self):
self._js = document
self.body = Element(document.body)
self.head = Element(document.head)
def __getitem__(self, key):
elements = self._js.querySelectorAll(key)
return ElementCollection([Element(el) for el in elements])
dom = PyDom()

File diff suppressed because it is too large Load Diff

View File

@@ -1,95 +0,0 @@
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()
]

View File

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

View File

@@ -1,95 +0,0 @@
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()
]

View File

@@ -1,569 +0,0 @@
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 = <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 = <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()

View File

@@ -1 +0,0 @@
from . import elements

View File

@@ -1,947 +0,0 @@
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.<attribute>`
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

View File

@@ -4,7 +4,7 @@ import time
from datetime import datetime as dt
from pyscript import display, when
from pyweb import pydom
from pyscript.web import dom
display(sys.version, target="system-info")
@@ -19,18 +19,15 @@ 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 = pydom["#result"]
btn = dom["#result"]
btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}"
@when("click", "#color-reset-button")
def reset_color(*args, **kwargs):
pydom["#result"].style["background-color"] = "white"
# btn_reset = pydom["#color-reset-button"][0].when('click', reset_color)
dom["#result"].style["background-color"] = "white"

View File

@@ -1,23 +1,13 @@
from unittest import mock
import pytest
from pyscript import document, when
from pyweb import pydom
from pyscript.web import dom
from pyscript.web import elements as el
from pyscript.web.elements import ElementCollection
class TestDocument:
def test__element(self):
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
assert dom.body._dom_element == document.body
assert dom.head._dom_element == document.head
def test_getitem_by_id():
@@ -26,14 +16,14 @@ def test_getitem_by_id():
txt = "You found test_id_selector"
selector = f"#{id_}"
# EXPECT the element to be found by id
result = pydom[selector]
result = dom.find(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
assert document.querySelector(selector).innerHTML == div.innerHTML == txt
# EXPECT the results to be of the right types
assert isinstance(div, pydom.BaseElement)
assert isinstance(result, pydom.ElementCollection)
assert isinstance(div, el.Element)
assert isinstance(result, ElementCollection)
def test_getitem_by_class():
@@ -43,7 +33,7 @@ def test_getitem_by_class():
"test_selector_w_children_child_1",
]
expected_class = "a-test-class"
result = pydom[f".{expected_class}"]
result = dom.find(f".{expected_class}")
div = result[0]
# EXPECT to find exact number of elements with the class in the page (== 3)
@@ -54,40 +44,40 @@ def test_getitem_by_class():
def test_read_n_write_collection_elements():
elements = pydom[".multi-elems"]
elements = dom.find(".multi-elems")
for element in elements:
assert element.html == f"Content {element.id.replace('#', '')}"
assert element.innerHTML == f"Content {element.id.replace('#', '')}"
new_content = "New Content"
elements.html = new_content
elements.innerHTML = new_content
for element in elements:
assert element.html == new_content
assert element.innerHTML == new_content
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 = pydom[f"#{id_}"][0]
parent_div = dom.find(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 BaseElement
assert isinstance(div, pydom.BaseElement)
# EXPECT the new element to be a el.Element
assert isinstance(div, el.Element)
# EXPECT the div attributes to be == to how they are configured in the page
assert div.html == "Child 1"
assert div.innerHTML == "Child 1"
assert div.id == "test_selector_w_children_child_1"
def test_equality(self):
# GIVEN 2 different Elements pointing to the same underlying element
id_ = "test_id_selector"
selector = f"#{id_}"
div = pydom[selector][0]
div2 = pydom[selector][0]
div = dom.find(selector)[0]
div2 = dom.find(selector)[0]
# EXPECT them to be equal
assert div == div2
@@ -95,34 +85,34 @@ class TestElement:
assert div is not div2
# EXPECT their value to always be equal
assert div.html == div2.html
div.html = "some value"
assert div.innerHTML == div2.innerHTML
div.innerHTML = "some value"
assert div.html == div2.html == "some value"
assert div.innerHTML == div2.innerHTML == "some value"
def test_append_element(self):
id_ = "element-append-tests"
div = pydom[f"#{id_}"][0]
div = dom.find(f"#{id_}")[0]
len_children_before = len(div.children)
new_el = div.create("p")
new_el = el.p("new element")
div.append(new_el)
assert len(div.children) == len_children_before + 1
assert div.children[-1] == new_el
def test_append_js_element(self):
def test_append_dom_element_element(self):
id_ = "element-append-tests"
div = pydom[f"#{id_}"][0]
div = dom.find(f"#{id_}")[0]
len_children_before = len(div.children)
new_el = div.create("p")
div.append(new_el._js)
new_el = el.p("new element")
div.append(new_el._dom_element)
assert len(div.children) == len_children_before + 1
assert div.children[-1] == new_el
def test_append_collection(self):
id_ = "element-append-tests"
div = pydom[f"#{id_}"][0]
div = dom.find(f"#{id_}")[0]
len_children_before = len(div.children)
collection = pydom[".collection"]
collection = dom.find(".collection")
div.append(collection)
assert len(div.children) == len_children_before + len(collection)
@@ -132,24 +122,24 @@ class TestElement:
def test_read_classes(self):
id_ = "test_class_selector"
expected_class = "a-test-class"
div = pydom[f"#{id_}"][0]
div = dom.find(f"#{id_}")[0]
assert div.classes == [expected_class]
def test_add_remove_class(self):
id_ = "div-no-classes"
classname = "tester-class"
div = pydom[f"#{id_}"][0]
div = dom.find(f"#{id_}")[0]
assert not div.classes
div.add_class(classname)
same_div = pydom[f"#{id_}"][0]
div.classes.add(classname)
same_div = dom.find(f"#{id_}")[0]
assert div.classes == [classname] == same_div.classes
div.remove_class(classname)
div.classes.remove(classname)
assert div.classes == [] == same_div.classes
def test_when_decorator(self):
called = False
just_a_button = pydom["#a-test-button"][0]
just_a_button = dom.find("#a-test-button")[0]
@when("click", just_a_button)
def on_click(event):
@@ -157,45 +147,49 @@ class TestElement:
called = True
# Now let's simulate a click on the button (using the low level JS API)
# so we don't risk pydom getting in the way
# so we don't risk dom getting in the way
assert not called
just_a_button._js.click()
just_a_button._dom_element.click()
assert called
def test_html_attribute(self):
# GIVEN an existing element on the page with a known empty text content
div = pydom["#element_attribute_tests"][0]
div = dom.find("#element_attribute_tests")[0]
# WHEN we set the html attribute
div.html = "<b>New Content</b>"
div.innerHTML = "<b>New Content</b>"
# EXPECT the element html and underlying JS Element innerHTML property
# to match what we expect and what
assert div.html == div._js.innerHTML == "<b>New Content</b>"
assert div.text == div._js.textContent == "New Content"
assert div.innerHTML == div._dom_element.innerHTML == "<b>New Content</b>"
assert div.text == div._dom_element.textContent == "New Content"
def test_text_attribute(self):
# GIVEN an existing element on the page with a known empty text content
div = pydom["#element_attribute_tests"][0]
div = dom.find("#element_attribute_tests")[0]
# WHEN we set the html attribute
div.text = "<b>New Content</b>"
# EXPECT the element html and underlying JS Element innerHTML property
# to match what we expect and what
assert div.html == div._js.innerHTML == "&lt;b&gt;New Content&lt;/b&gt;"
assert div.text == div._js.textContent == "<b>New Content</b>"
assert (
div.innerHTML
== div._dom_element.innerHTML
== "&lt;b&gt;New Content&lt;/b&gt;"
)
assert div.text == div._dom_element.textContent == "<b>New Content</b>"
class TestCollection:
def test_iter_eq_children(self):
elements = pydom[".multi-elems"]
elements = dom.find(".multi-elems")
assert [el for el in elements] == [el for el in elements.children]
assert len(elements) == 3
def test_slices(self):
elements = pydom[".multi-elems"]
elements = dom.find(".multi-elems")
assert elements[0]
_slice = elements[:2]
assert len(_slice) == 2
@@ -205,26 +199,26 @@ class TestCollection:
def test_style_rule(self):
selector = ".multi-elems"
elements = pydom[selector]
elements = dom.find(selector)
for el in elements:
assert el.style["background-color"] != "red"
elements.style["background-color"] = "red"
for i, el in enumerate(pydom[selector]):
for i, el in enumerate(dom.find(selector)):
assert elements[i].style["background-color"] == "red"
assert el.style["background-color"] == "red"
elements.style.remove("background-color")
for i, el in enumerate(pydom[selector]):
for i, el in enumerate(dom.find(selector)):
assert el.style["background-color"] != "red"
assert elements[i].style["background-color"] != "red"
def test_when_decorator(self):
called = False
buttons_collection = pydom["button"]
buttons_collection = dom.find("button")
@when("click", buttons_collection)
def on_click(event):
@@ -232,42 +226,44 @@ class TestCollection:
called = True
# Now let's simulate a click on the button (using the low level JS API)
# so we don't risk pydom getting in the way
# so we don't risk dom getting in the way
assert not called
for button in buttons_collection:
button._js.click()
button._dom_element.click()
assert called
called = False
class TestCreation:
def test_create_document_element(self):
new_el = pydom.create("div")
# TODO: This test should probably be removed since it's testing the elements module
new_el = el.div("new element")
new_el.id = "new_el_id"
assert isinstance(new_el, pydom.BaseElement)
assert new_el._js.tagName == "DIV"
assert isinstance(new_el, el.Element)
assert new_el._dom_element.tagName == "DIV"
# EXPECT the new element to be associated with the document
assert new_el.parent == None
pydom.body.append(new_el)
dom.body.append(new_el)
assert pydom["#new_el_id"][0].parent == pydom.body
assert dom.find("#new_el_id")[0].parent == dom.body
def test_create_element_child(self):
selector = "#element-creation-test"
parent_div = pydom[selector][0]
parent_div = dom.find(selector)[0]
# Creating an element from another element automatically creates that element
# as a child of the original element
new_el = parent_div.create(
"p", classes=["code-description"], html="Ciao PyScripters!"
new_el = el.p(
"a div", classes=["code-description"], innerHTML="Ciao PyScripters!"
)
parent_div.append(new_el)
assert isinstance(new_el, el.Element)
assert new_el._dom_element.tagName == "P"
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 pydom[selector][0].children[0] == new_el
assert dom.find(selector)[0].children[0] == new_el
class TestInput:
@@ -281,10 +277,10 @@ class TestInput:
def test_value(self):
for id_ in self.input_ids:
expected_type = id_.split("_")[-1]
result = pydom[f"#{id_}"]
result = dom.find(f"#{id_}")
input_el = result[0]
assert input_el._js.type == expected_type
assert input_el.value == f"Content {id_}" == input_el._js.value
assert input_el._dom_element.type == expected_type
assert input_el.value == f"Content {id_}" == input_el._dom_element.value
# Check that we can set the value
new_value = f"New Value {expected_type}"
@@ -299,7 +295,7 @@ class TestInput:
def test_set_value_collection(self):
for id_ in self.input_ids:
input_el = pydom[f"#{id_}"]
input_el = dom.find(f"#{id_}")
assert input_el.value[0] == f"Content {id_}" == input_el[0].value
@@ -307,36 +303,35 @@ class TestInput:
input_el.value = new_value
assert input_el.value[0] == new_value == input_el[0].value
def test_element_without_value(self):
result = pydom[f"#tests-terminal"][0]
with pytest.raises(AttributeError):
result.value = "some value"
def test_element_without_collection(self):
result = pydom[f"#tests-terminal"]
with pytest.raises(AttributeError):
result.value = "some value"
def test_element_without_collection(self):
result = pydom[f"#tests-terminal"]
with pytest.raises(AttributeError):
result.value = "some value"
# TODO: We only attach attributes to the classes that have them now which means we
# would have to have some other way to help users if using attributes that aren't
# actually on the class. Maybe a job for __setattr__?
#
# def test_element_without_value(self):
# result = dom.find(f"#tests-terminal"][0]
# with pytest.raises(AttributeError):
# result.value = "some value"
#
# def test_element_without_value_via_collection(self):
# result = dom.find(f"#tests-terminal"]
# with pytest.raises(AttributeError):
# result.value = "some value"
class TestSelect:
def test_select_options_iter(self):
select = pydom[f"#test_select_element_w_options"][0]
select = dom.find(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}"
assert option.innerHTML == f"Option {i}"
def test_select_options_len(self):
select = pydom[f"#test_select_element_w_options"][0]
select = dom.find(f"#test_select_element_w_options")[0]
assert len(select.options) == 2
def test_select_options_clear(self):
select = pydom[f"#test_select_element_to_clear"][0]
select = dom.find(f"#test_select_element_to_clear")[0]
assert len(select.options) == 3
select.options.clear()
@@ -345,7 +340,7 @@ class TestSelect:
def test_select_element_add(self):
# GIVEN the existing select element with no options
select = pydom[f"#test_select_element"][0]
select = dom.find(f"#test_select_element")[0]
# EXPECT the select element to have no options
assert len(select.options) == 0
@@ -357,7 +352,7 @@ class TestSelect:
# we passed in
assert len(select.options) == 1
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[0].innerHTML == "Option 1"
# WHEN we add another option (blank this time)
select.options.add("")
@@ -367,7 +362,7 @@ class TestSelect:
# EXPECT the last option to have an empty value and html
assert select.options[1].value == ""
assert select.options[1].html == ""
assert select.options[1].innerHTML == ""
# WHEN we add another option (this time adding it in between the other 2
# options by using an integer index)
@@ -378,11 +373,11 @@ class TestSelect:
# EXPECT the middle option to have the value and html we passed in
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[0].innerHTML == "Option 1"
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[1].innerHTML == "Option 2"
assert select.options[2].value == ""
assert select.options[2].html == ""
assert select.options[2].innerHTML == ""
# WHEN we add another option (this time adding it in between the other 2
# options but using the option itself)
@@ -395,38 +390,48 @@ class TestSelect:
# EXPECT the middle option to have the value and html we passed in
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[0].selected == select.options[0]._js.selected == False
assert select.options[0].innerHTML == "Option 1"
assert (
select.options[0].selected
== select.options[0]._dom_element.selected
== False
)
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[1].innerHTML == "Option 2"
assert select.options[2].value == "3"
assert select.options[2].html == "Option 3"
assert select.options[2].selected == select.options[2]._js.selected == True
assert select.options[2].innerHTML == "Option 3"
assert (
select.options[2].selected
== select.options[2]._dom_element.selected
== True
)
assert select.options[3].value == ""
assert select.options[3].html == ""
assert select.options[3].innerHTML == ""
# WHEN we add another option (this time adding it in between the other 2
# options but using the JS element of the option itself)
select.options.add(value="2a", html="Option 2a", before=select.options[2]._js)
select.options.add(
value="2a", html="Option 2a", before=select.options[2]._dom_element
)
# EXPECT the select element to have 3 options
assert len(select.options) == 5
# EXPECT the middle option to have the value and html we passed in
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[0].innerHTML == "Option 1"
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[1].innerHTML == "Option 2"
assert select.options[2].value == "2a"
assert select.options[2].html == "Option 2a"
assert select.options[2].innerHTML == "Option 2a"
assert select.options[3].value == "3"
assert select.options[3].html == "Option 3"
assert select.options[3].innerHTML == "Option 3"
assert select.options[4].value == ""
assert select.options[4].html == ""
assert select.options[4].innerHTML == ""
def test_select_options_remove(self):
# GIVEN the existing select element with 3 options
select = pydom[f"#test_select_element_to_remove"][0]
select = dom.find(f"#test_select_element_to_remove")[0]
# EXPECT the select element to have 3 options
assert len(select.options) == 4
@@ -448,12 +453,12 @@ class TestSelect:
def test_select_get_selected_option(self):
# GIVEN the existing select element with one selected option
select = pydom[f"#test_select_element_w_options"][0]
select = dom.find(f"#test_select_element_w_options")[0]
# WHEN we get the selected option
selected_option = select.options.selected
# EXPECT the selected option to be correct
assert selected_option.value == "2"
assert selected_option.html == "Option 2"
assert selected_option.selected == selected_option._js.selected == True
assert selected_option.innerHTML == "Option 2"
assert selected_option.selected == selected_option._dom_element.selected == True

View File

@@ -101,10 +101,11 @@ class TestElements(PyScriptTest):
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyweb import pydom
from pyweb.ui.elements import {el_type}
from pyscript.web import dom
from pyscript.web.elements import {el_type}
el = {el_type}({attributes})
pydom.body.append(el)
dom.body.append(el)
</script>
"""
self.pyscript_run(code_)
@@ -178,7 +179,7 @@ class TestElements(PyScriptTest):
)
def test_b(self, interpreter):
self._create_el_and_basic_asserts("aside", "some text", interpreter)
self._create_el_and_basic_asserts("b", "some text", interpreter)
def test_blockquote(self, interpreter):
self._create_el_and_basic_asserts("blockquote", "some text", interpreter)
@@ -603,3 +604,207 @@ class TestElements(PyScriptTest):
properties=properties,
expected_errors=self.expected_missing_file_errors,
)
def test_append_py_element(self, interpreter):
# Let's make sure the body of the page is clean first
body = self.page.locator("body")
assert body.inner_html() == ""
# Let's make sure the element is not in the page
element = self.page.locator("div")
assert not element.count()
div_text_content = "Luke, I am your father"
p_text_content = "noooooooooo!"
# Let's create the element
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript.web import dom
from pyscript.web.elements import div, p
el = div("{div_text_content}")
child = p('{p_text_content}')
el.append(child)
dom.body.append(el)
</script>
"""
self.pyscript_run(code_)
# Let's keep the tag in 2 variables, one for the selector and another to
# check the return tag from the selector
el = self.page.locator("div")
tag = el.evaluate("node => node.tagName")
assert tag == "DIV"
assert el.text_content() == f"{div_text_content}{p_text_content}"
assert (
el.evaluate("node => node.children.length") == 1
), "There should be only 1 child"
assert el.evaluate("node => node.children[0].tagName") == "P"
assert (
el.evaluate("node => node.children[0].parentNode.textContent")
== f"{div_text_content}{p_text_content}"
)
assert el.evaluate("node => node.children[0].textContent") == p_text_content
def test_append_proxy_element(self, interpreter):
# Let's make sure the body of the page is clean first
body = self.page.locator("body")
assert body.inner_html() == ""
# Let's make sure the element is not in the page
element = self.page.locator("div")
assert not element.count()
div_text_content = "Luke, I am your father"
p_text_content = "noooooooooo!"
# Let's create the element
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript import document
from pyscript.web import dom
from pyscript.web.elements import div, p
el = div("{div_text_content}")
child = document.createElement('P')
child.textContent = '{p_text_content}'
el.append(child)
dom.body.append(el)
</script>
"""
self.pyscript_run(code_)
# Let's keep the tag in 2 variables, one for the selector and another to
# check the return tag from the selector
el = self.page.locator("div")
tag = el.evaluate("node => node.tagName")
assert tag == "DIV"
assert el.text_content() == f"{div_text_content}{p_text_content}"
assert (
el.evaluate("node => node.children.length") == 1
), "There should be only 1 child"
assert el.evaluate("node => node.children[0].tagName") == "P"
assert (
el.evaluate("node => node.children[0].parentNode.textContent")
== f"{div_text_content}{p_text_content}"
)
assert el.evaluate("node => node.children[0].textContent") == p_text_content
def test_append_py_elementcollection(self, interpreter):
# Let's make sure the body of the page is clean first
body = self.page.locator("body")
assert body.inner_html() == ""
# Let's make sure the element is not in the page
element = self.page.locator("div")
assert not element.count()
div_text_content = "Luke, I am your father"
p_text_content = "noooooooooo!"
p2_text_content = "not me!"
# Let's create the element
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript.web import dom
from pyscript.web.elements import div, p, ElementCollection
el = div("{div_text_content}")
child1 = p('{p_text_content}')
child2 = p('{p2_text_content}', id='child2')
collection = ElementCollection([child1, child2])
el.append(collection)
dom.body.append(el)
</script>
"""
self.pyscript_run(code_)
# Let's keep the tag in 2 variables, one for the selector and another to
# check the return tag from the selector
el = self.page.locator("div")
tag = el.evaluate("node => node.tagName")
assert tag == "DIV"
parent_full_content = f"{div_text_content}{p_text_content}{p2_text_content}"
assert el.text_content() == parent_full_content
assert (
el.evaluate("node => node.children.length") == 2
), "There should be only 1 child"
assert el.evaluate("node => node.children[0].tagName") == "P"
assert (
el.evaluate("node => node.children[0].parentNode.textContent")
== parent_full_content
)
assert el.evaluate("node => node.children[0].textContent") == p_text_content
assert el.evaluate("node => node.children[1].tagName") == "P"
assert el.evaluate("node => node.children[1].id") == "child2"
assert (
el.evaluate("node => node.children[1].parentNode.textContent")
== parent_full_content
)
assert el.evaluate("node => node.children[1].textContent") == p2_text_content
def test_append_js_element_nodelist(self, interpreter):
# Let's make sure the body of the page is clean first
body = self.page.locator("body")
assert body.inner_html() == ""
# Let's make sure the element is not in the page
element = self.page.locator("div")
assert not element.count()
div_text_content = "Luke, I am your father"
p_text_content = "noooooooooo!"
p2_text_content = "not me!"
# Let's create the element
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript import document
from pyscript.web import dom
from pyscript.web.elements import div, p, ElementCollection
el = div("{div_text_content}")
child1 = p('{p_text_content}')
child2 = p('{p2_text_content}', id='child2')
dom.body.append(child1)
dom.body.append(child2)
nodes = document.querySelectorAll('p')
el.append(nodes)
dom.body.append(el)
</script>
"""
self.pyscript_run(code_)
# Let's keep the tag in 2 variables, one for the selector and another to
# check the return tag from the selector
el = self.page.locator("div")
tag = el.evaluate("node => node.tagName")
assert tag == "DIV"
parent_full_content = f"{div_text_content}{p_text_content}{p2_text_content}"
assert el.text_content() == parent_full_content
assert (
el.evaluate("node => node.children.length") == 2
), "There should be only 1 child"
assert el.evaluate("node => node.children[0].tagName") == "P"
assert (
el.evaluate("node => node.children[0].parentNode.textContent")
== parent_full_content
)
assert el.evaluate("node => node.children[0].textContent") == p_text_content
assert el.evaluate("node => node.children[1].tagName") == "P"
assert el.evaluate("node => node.children[1].id") == "child2"
assert (
el.evaluate("node => node.children[1].parentNode.textContent")
== parent_full_content
)
assert el.evaluate("node => node.children[1].textContent") == p2_text_content

View File

@@ -11,21 +11,10 @@ declare namespace _default {
"util.py": string;
web: {
"__init__.py": string;
"dom.py": string;
"elements.py": string;
"media.py": string;
};
"websocket.py": string;
"workers.py": string;
};
let pyweb: {
"__init__.py": string;
"media.py": string;
"pydom.py": string;
ui: {
"__init__.py": string;
"elements.py": string;
};
};
}
export default _default;