Bring back pyweb as it was (#2105)

This commit is contained in:
Andrea Giammarchi
2024-06-21 14:49:20 +02:00
committed by GitHub
parent 15c19aa708
commit 4b90ebdef5
12 changed files with 1704 additions and 71 deletions

View File

@@ -1,12 +1,12 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.4.48", "version": "0.4.50",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.4.48", "version": "0.4.50",
"license": "APACHE-2.0", "license": "APACHE-2.0",
"dependencies": { "dependencies": {
"@ungap/with-resolvers": "^0.1.0", "@ungap/with-resolvers": "^0.1.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.4.48", "version": "0.4.50",
"type": "module", "type": "module",
"description": "PyScript", "description": "PyScript",
"module": "./index.js", "module": "./index.js",

View File

@@ -19,17 +19,16 @@ def when(event_type=None, selector=None):
""" """
def decorator(func): def decorator(func):
from pyscript.web.elements import Element, ElementCollection
if isinstance(selector, str): if isinstance(selector, str):
elements = document.querySelectorAll(selector) elements = document.querySelectorAll(selector)
else: else:
# TODO: This is a hack that will be removed when pyscript becomes a package # TODO: This is a hack that will be removed when pyscript becomes a package
# and we can better manage the imports without circular dependencies # 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] elements = [selector._js]
elif isinstance(selector, ElementCollection): elif isinstance(selector, pydom.ElementCollection):
elements = [el._js for el in selector] elements = [el._js for el in selector]
else: else:
raise ValueError( raise ValueError(

View File

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

View File

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

View File

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

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

View File

@@ -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.<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 datetime import datetime as dt
from pyscript import display, when from pyscript import display, when
from pyscript.web import dom from pyweb import pydom
display(sys.version, target="system-info") display(sys.version, target="system-info")
@@ -19,15 +19,18 @@ def on_click():
tstr = "{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}" tstr = "{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}"
timenow = tstr.format(tnow[2], tnow[1], tnow[0], *tnow[2:]) 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") @when("click", "#color-button")
def on_color_click(event): def on_color_click(event):
btn = dom["#result"] btn = pydom["#result"]
btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}" btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}"
@when("click", "#color-reset-button") @when("click", "#color-reset-button")
def reset_color(*args, **kwargs): 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)

View File

@@ -2,13 +2,22 @@ from unittest import mock
import pytest import pytest
from pyscript import document, when from pyscript import document, when
from pyscript.web import dom from pyweb import pydom
from pyscript.web import elements as el
class TestDocument: class TestDocument:
def test__element(self): 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(): def test_getitem_by_id():
@@ -17,14 +26,14 @@ def test_getitem_by_id():
txt = "You found test_id_selector" txt = "You found test_id_selector"
selector = f"#{id_}" selector = f"#{id_}"
# EXPECT the element to be found by id # EXPECT the element to be found by id
result = dom[selector] result = pydom[selector]
div = result[0] div = result[0]
# EXPECT the element text value to match what we expect and what # EXPECT the element text value to match what we expect and what
# the JS document.querySelector API would return # the JS document.querySelector API would return
assert document.querySelector(selector).innerHTML == div.html == txt assert document.querySelector(selector).innerHTML == div.html == txt
# EXPECT the results to be of the right types # EXPECT the results to be of the right types
assert isinstance(div, el.BaseElement) assert isinstance(div, pydom.BaseElement)
assert isinstance(result, dom.ElementCollection) assert isinstance(result, pydom.ElementCollection)
def test_getitem_by_class(): def test_getitem_by_class():
@@ -34,7 +43,7 @@ def test_getitem_by_class():
"test_selector_w_children_child_1", "test_selector_w_children_child_1",
] ]
expected_class = "a-test-class" expected_class = "a-test-class"
result = dom[f".{expected_class}"] result = pydom[f".{expected_class}"]
div = result[0] div = result[0]
# EXPECT to find exact number of elements with the class in the page (== 3) # 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(): def test_read_n_write_collection_elements():
elements = dom[".multi-elems"] elements = pydom[".multi-elems"]
for element in elements: for element in elements:
assert element.html == f"Content {element.id.replace('#', '')}" assert element.html == f"Content {element.id.replace('#', '')}"
@@ -60,15 +69,15 @@ class TestElement:
def test_query(self): def test_query(self):
# GIVEN an existing element on the page, with at least 1 child element # GIVEN an existing element on the page, with at least 1 child element
id_ = "test_selector_w_children" 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 # EXPECT it to be able to query for the first child element
div = parent_div.find("div")[0] div = parent_div.find("div")[0]
# EXPECT the new element to be associated with the parent # EXPECT the new element to be associated with the parent
assert div.parent == parent_div assert div.parent == parent_div
# EXPECT the new element to be a el.BaseElement # EXPECT the new element to be a BaseElement
assert isinstance(div, el.BaseElement) assert isinstance(div, pydom.BaseElement)
# EXPECT the div attributes to be == to how they are configured in the page # EXPECT the div attributes to be == to how they are configured in the page
assert div.html == "Child 1" assert div.html == "Child 1"
assert div.id == "test_selector_w_children_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 # GIVEN 2 different Elements pointing to the same underlying element
id_ = "test_id_selector" id_ = "test_id_selector"
selector = f"#{id_}" selector = f"#{id_}"
div = dom[selector][0] div = pydom[selector][0]
div2 = dom[selector][0] div2 = pydom[selector][0]
# EXPECT them to be equal # EXPECT them to be equal
assert div == div2 assert div == div2
@@ -93,27 +102,27 @@ class TestElement:
def test_append_element(self): def test_append_element(self):
id_ = "element-append-tests" id_ = "element-append-tests"
div = dom[f"#{id_}"][0] div = pydom[f"#{id_}"][0]
len_children_before = len(div.children) len_children_before = len(div.children)
new_el = el.p("new element") new_el = div.create("p")
div.append(new_el) div.append(new_el)
assert len(div.children) == len_children_before + 1 assert len(div.children) == len_children_before + 1
assert div.children[-1] == new_el assert div.children[-1] == new_el
def test_append_js_element(self): def test_append_js_element(self):
id_ = "element-append-tests" id_ = "element-append-tests"
div = dom[f"#{id_}"][0] div = pydom[f"#{id_}"][0]
len_children_before = len(div.children) len_children_before = len(div.children)
new_el = el.p("new element") new_el = div.create("p")
div.append(new_el._js) div.append(new_el._js)
assert len(div.children) == len_children_before + 1 assert len(div.children) == len_children_before + 1
assert div.children[-1] == new_el assert div.children[-1] == new_el
def test_append_collection(self): def test_append_collection(self):
id_ = "element-append-tests" id_ = "element-append-tests"
div = dom[f"#{id_}"][0] div = pydom[f"#{id_}"][0]
len_children_before = len(div.children) len_children_before = len(div.children)
collection = dom[".collection"] collection = pydom[".collection"]
div.append(collection) div.append(collection)
assert len(div.children) == len_children_before + len(collection) assert len(div.children) == len_children_before + len(collection)
@@ -123,16 +132,16 @@ class TestElement:
def test_read_classes(self): def test_read_classes(self):
id_ = "test_class_selector" id_ = "test_class_selector"
expected_class = "a-test-class" expected_class = "a-test-class"
div = dom[f"#{id_}"][0] div = pydom[f"#{id_}"][0]
assert div.classes == [expected_class] assert div.classes == [expected_class]
def test_add_remove_class(self): def test_add_remove_class(self):
id_ = "div-no-classes" id_ = "div-no-classes"
classname = "tester-class" classname = "tester-class"
div = dom[f"#{id_}"][0] div = pydom[f"#{id_}"][0]
assert not div.classes assert not div.classes
div.add_class(classname) div.add_class(classname)
same_div = dom[f"#{id_}"][0] same_div = pydom[f"#{id_}"][0]
assert div.classes == [classname] == same_div.classes assert div.classes == [classname] == same_div.classes
div.remove_class(classname) div.remove_class(classname)
assert div.classes == [] == same_div.classes assert div.classes == [] == same_div.classes
@@ -140,7 +149,7 @@ class TestElement:
def test_when_decorator(self): def test_when_decorator(self):
called = False called = False
just_a_button = dom["#a-test-button"][0] just_a_button = pydom["#a-test-button"][0]
@when("click", just_a_button) @when("click", just_a_button)
def on_click(event): def on_click(event):
@@ -148,7 +157,7 @@ class TestElement:
called = True called = True
# Now let's simulate a click on the button (using the low level JS API) # 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 assert not called
just_a_button._js.click() just_a_button._js.click()
@@ -156,7 +165,7 @@ class TestElement:
def test_html_attribute(self): def test_html_attribute(self):
# GIVEN an existing element on the page with a known empty text content # 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 # WHEN we set the html attribute
div.html = "<b>New Content</b>" div.html = "<b>New Content</b>"
@@ -168,7 +177,7 @@ class TestElement:
def test_text_attribute(self): def test_text_attribute(self):
# GIVEN an existing element on the page with a known empty text content # 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 # WHEN we set the html attribute
div.text = "<b>New Content</b>" div.text = "<b>New Content</b>"
@@ -181,12 +190,12 @@ class TestElement:
class TestCollection: class TestCollection:
def test_iter_eq_children(self): 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 [el for el in elements] == [el for el in elements.children]
assert len(elements) == 3 assert len(elements) == 3
def test_slices(self): def test_slices(self):
elements = dom[".multi-elems"] elements = pydom[".multi-elems"]
assert elements[0] assert elements[0]
_slice = elements[:2] _slice = elements[:2]
assert len(_slice) == 2 assert len(_slice) == 2
@@ -196,26 +205,26 @@ class TestCollection:
def test_style_rule(self): def test_style_rule(self):
selector = ".multi-elems" selector = ".multi-elems"
elements = dom[selector] elements = pydom[selector]
for el in elements: for el in elements:
assert el.style["background-color"] != "red" assert el.style["background-color"] != "red"
elements.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 elements[i].style["background-color"] == "red"
assert el.style["background-color"] == "red" assert el.style["background-color"] == "red"
elements.style.remove("background-color") 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 el.style["background-color"] != "red"
assert elements[i].style["background-color"] != "red" assert elements[i].style["background-color"] != "red"
def test_when_decorator(self): def test_when_decorator(self):
called = False called = False
buttons_collection = dom["button"] buttons_collection = pydom["button"]
@when("click", buttons_collection) @when("click", buttons_collection)
def on_click(event): def on_click(event):
@@ -223,7 +232,7 @@ class TestCollection:
called = True called = True
# Now let's simulate a click on the button (using the low level JS API) # 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 assert not called
for button in buttons_collection: for button in buttons_collection:
button._js.click() button._js.click()
@@ -233,32 +242,32 @@ class TestCollection:
class TestCreation: class TestCreation:
def test_create_document_element(self): def test_create_document_element(self):
# TODO: This test should probably be removed since it's testing the elements module new_el = pydom.create("div")
new_el = el.div("new element")
new_el.id = "new_el_id" new_el.id = "new_el_id"
assert isinstance(new_el, el.BaseElement) assert isinstance(new_el, pydom.BaseElement)
assert new_el._js.tagName == "DIV" assert new_el._js.tagName == "DIV"
# EXPECT the new element to be associated with the document # EXPECT the new element to be associated with the document
assert new_el.parent == None 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): def test_create_element_child(self):
selector = "#element-creation-test" selector = "#element-creation-test"
parent_div = dom[selector][0] parent_div = pydom[selector][0]
# Creating an element from another element automatically creates that element # Creating an element from another element automatically creates that element
# as a child of the original element # as a child of the original element
new_el = el.p("a div", classes=["code-description"], html="Ciao PyScripters!") new_el = parent_div.create(
parent_div.append(new_el) "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" assert new_el._js.tagName == "P"
# EXPECT the new element to be associated with the document # EXPECT the new element to be associated with the document
assert new_el.parent == parent_div assert new_el.parent == parent_div
assert dom[selector][0].children[0] == new_el
assert pydom[selector][0].children[0] == new_el
class TestInput: class TestInput:
@@ -272,7 +281,7 @@ class TestInput:
def test_value(self): def test_value(self):
for id_ in self.input_ids: for id_ in self.input_ids:
expected_type = id_.split("_")[-1] expected_type = id_.split("_")[-1]
result = dom[f"#{id_}"] result = pydom[f"#{id_}"]
input_el = result[0] input_el = result[0]
assert input_el._js.type == expected_type assert input_el._js.type == expected_type
assert input_el.value == f"Content {id_}" == input_el._js.value assert input_el.value == f"Content {id_}" == input_el._js.value
@@ -290,7 +299,7 @@ class TestInput:
def test_set_value_collection(self): def test_set_value_collection(self):
for id_ in self.input_ids: 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 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 assert input_el.value[0] == new_value == input_el[0].value
def test_element_without_value(self): def test_element_without_value(self):
result = dom[f"#tests-terminal"][0] result = pydom[f"#tests-terminal"][0]
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
result.value = "some value" result.value = "some value"
def test_element_without_collection(self): def test_element_without_collection(self):
result = dom[f"#tests-terminal"] result = pydom[f"#tests-terminal"]
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
result.value = "some value" result.value = "some value"
def test_element_without_collection(self): def test_element_without_collection(self):
result = dom[f"#tests-terminal"] result = pydom[f"#tests-terminal"]
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
result.value = "some value" result.value = "some value"
class TestSelect: class TestSelect:
def test_select_options_iter(self): 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): for i, option in enumerate(select.options, 1):
assert option.value == f"{i}" assert option.value == f"{i}"
assert option.html == f"Option {i}" assert option.html == f"Option {i}"
def test_select_options_len(self): 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 assert len(select.options) == 2
def test_select_options_clear(self): 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 assert len(select.options) == 3
select.options.clear() select.options.clear()
@@ -336,7 +345,7 @@ class TestSelect:
def test_select_element_add(self): def test_select_element_add(self):
# GIVEN the existing select element with no options # 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 # EXPECT the select element to have no options
assert len(select.options) == 0 assert len(select.options) == 0
@@ -417,7 +426,7 @@ class TestSelect:
def test_select_options_remove(self): def test_select_options_remove(self):
# GIVEN the existing select element with 3 options # 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 # EXPECT the select element to have 3 options
assert len(select.options) == 4 assert len(select.options) == 4
@@ -439,7 +448,7 @@ class TestSelect:
def test_select_get_selected_option(self): def test_select_get_selected_option(self):
# GIVEN the existing select element with one selected option # 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 # WHEN we get the selected option
selected_option = select.options.selected selected_option = select.options.selected

View File

@@ -101,11 +101,10 @@ class TestElements(PyScriptTest):
code_ = f""" code_ = f"""
from pyscript import when from pyscript import when
<script type="{interpreter}"> <script type="{interpreter}">
from pyscript.web import dom from pyweb import pydom
from pyscript.web.elements import {el_type} from pyweb.ui.elements import {el_type}
el = {el_type}({attributes}) el = {el_type}({attributes})
dom.body.append(el) pydom.body.append(el)
</script> </script>
""" """
self.pyscript_run(code_) self.pyscript_run(code_)

View File

@@ -17,5 +17,14 @@ declare namespace _default {
}; };
"websocket.py": string; "websocket.py": string;
}; };
let pyweb: {
"__init__.py": string;
"media.py": string;
"pydom.py": string;
ui: {
"__init__.py": string;
"elements.py": string;
};
};
} }
export default _default; export default _default;