From d456b370c7598bb2e8f05ccb0b2d8e30615604ca Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 24 Jul 2024 13:38:04 -0500 Subject: [PATCH 01/34] Minor cleanups: move all Element classes to bottom of module. --- .../src/stdlib/pyscript/web/elements.py | 337 +++++++++--------- 1 file changed, 168 insertions(+), 169 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index d27f64487de..6b77c9975b4 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -25,12 +25,9 @@ class Element: def from_dom_element(cls, dom_element): """Create an instance of a subclass of `Element` for a DOM element.""" - element_cls = ELEMENT_CLASSES_BY_TAG_NAME.get(dom_element.tagName.lower()) - - # For any unknown elements (custom tags etc.) create an instance of this - # class ('Element'). - if not element_cls: - element_cls = cls + # Lookup the element class by tag name and for any unknown elements (custom + # tags etc.) use this class (`Element`). + element_cls = ELEMENT_CLASSES_BY_TAG_NAME.get(dom_element.tagName.lower(), cls) return element_cls(dom_element=dom_element) @@ -324,7 +321,7 @@ def remove(self, item: int) -> None: def clear(self) -> None: """Remove all the options""" - for i in range(len(self)): + for index in range(len(self)): self.remove(0) @property @@ -403,7 +400,170 @@ def __init__( self.innerHTML += child -# Classes for every element type. If the element type (e.g. "input") clashes with +class ClassesCollection: + def __init__(self, collection: "ElementCollection") -> None: + self._collection = collection + + def __contains__(self, class_name): + for element in self._collection: + if class_name in element.classes: + return True + + return False + + def __eq__(self, other): + return ( + isinstance(other, ClassesCollection) + and self._collection == other._collection + ) + + def __iter__(self): + for class_name in self._all_class_names(): + yield class_name + + def __len__(self): + return len(self._all_class_names()) + + def __repr__(self): + return f"ClassesCollection({repr(self._collection)})" + + def __str__(self): + return " ".join(self._all_class_names()) + + def add(self, *class_names): + for element in self._collection: + element.classes.add(*class_names) + + def contains(self, class_name): + return class_name in self + + def remove(self, *class_names): + for element in self._collection: + element.classes.remove(*class_names) + + def replace(self, old_class, new_class): + for element in self._collection: + element.classes.replace(old_class, new_class) + + def toggle(self, *class_names): + for element in self._collection: + element.classes.toggle(*class_names) + + def _all_class_names(self): + all_class_names = set() + for element in self._collection: + for class_name in element.classes: + all_class_names.add(class_name) + + return all_class_names + + +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 __repr__(self): + return f"StyleCollection({repr(self._collection)})" + + 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._classes = ClassesCollection(self) + self._style = StyleCollection(self) + + def __eq__(self, obj): + """Check for equality by comparing the underlying DOM elements.""" + return isinstance(obj, ElementCollection) and obj._elements == self._elements + + 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 query selector. + return self.find(key) + + def __iter__(self): + yield from self._elements + + def __len__(self): + return len(self._elements) + + def __repr__(self): + return ( + f"{self.__class__.__name__} (length: {len(self._elements)}) " + f"{self._elements}" + ) + + def __getattr__(self, item): + return self._get_attribute(item) + + def __setattr__(self, key, value): + # This class overrides `__setattr__` to delegate "public" attributes to the + # elements in the collection. BUT, we don't use the usual Python pattern where + # we set attributes on the collection itself via `self.__dict__` as that is not + # yet supported in our build of MicroPython. Instead, we handle it here by + # using super for all "private" attributes (those starting with an underscore). + if key.startswith("_"): + super().__setattr__(key, value) + + else: + self._set_attribute(key, value) + + @property + def children(self): + return self._elements + + @property + def classes(self): + return self._classes + + @property + def style(self): + return self._style + + def find(self, selector): + elements = [] + for element in self._elements: + elements.extend(element.find(selector)) + + return ElementCollection(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) + + +######################################################################################## + +# Classes for every HTML element type. If the element type (e.g. "input") clashes with # either a Python keyword or common symbol, then we suffix the class name with an "_" # (e.g. "input_"). @@ -905,167 +1065,6 @@ class wbr(Element): """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr""" -class ClassesCollection: - def __init__(self, collection: "ElementCollection") -> None: - self._collection = collection - - def __contains__(self, class_name): - for element in self._collection: - if class_name in element.classes: - return True - - return False - - def __eq__(self, other): - return ( - isinstance(other, ClassesCollection) - and self._collection == other._collection - ) - - def __iter__(self): - for class_name in self._all_class_names(): - yield class_name - - def __len__(self): - return len(self._all_class_names()) - - def __repr__(self): - return f"ClassesCollection({repr(self._collection)})" - - def __str__(self): - return " ".join(self._all_class_names()) - - def add(self, *class_names): - for element in self._collection: - element.classes.add(*class_names) - - def contains(self, class_name): - return class_name in self - - def remove(self, *class_names): - for element in self._collection: - element.classes.remove(*class_names) - - def replace(self, old_class, new_class): - for element in self._collection: - element.classes.replace(old_class, new_class) - - def toggle(self, *class_names): - for element in self._collection: - element.classes.toggle(*class_names) - - def _all_class_names(self): - all_class_names = set() - for element in self._collection: - for class_name in element.classes: - all_class_names.add(class_name) - - return all_class_names - - -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 __repr__(self): - return f"StyleCollection({repr(self._collection)})" - - 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._classes = ClassesCollection(self) - self._style = StyleCollection(self) - - def __eq__(self, obj): - """Check for equality by comparing the underlying DOM elements.""" - return isinstance(obj, ElementCollection) and obj._elements == self._elements - - 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 query selector. - return self.find(key) - - def __iter__(self): - yield from self._elements - - def __len__(self): - return len(self._elements) - - def __repr__(self): - return ( - f"{self.__class__.__name__} (length: {len(self._elements)}) " - f"{self._elements}" - ) - - def __getattr__(self, item): - return self._get_attribute(item) - - def __setattr__(self, key, value): - # This class overrides `__setattr__` to delegate "public" attributes to the - # elements in the collection. BUT, we don't use the usual Python pattern where - # we set attributes on the collection itself via `self.__dict__` as that is not - # yet supported in our build of MicroPython. Instead, we handle it here by - # using super for all "private" attributes (those starting with an underscore). - if key.startswith("_"): - super().__setattr__(key, value) - - else: - self._set_attribute(key, value) - - @property - def children(self): - return self._elements - - @property - def classes(self): - return self._classes - - @property - def style(self): - return self._style - - def find(self, selector): - elements = [] - for element in self._elements: - elements.extend(element.find(selector)) - - return ElementCollection(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) - - # fmt: off ELEMENT_CLASSES = [ a, abbr, address, area, article, aside, audio, From 73fedddf923274ab51554d5a8b3f996461804029 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 24 Jul 2024 14:43:29 -0500 Subject: [PATCH 02/34] Commenting. --- pyscript.core/src/stdlib/pyscript/web/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index 6b77c9975b4..5e98a436afc 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -563,7 +563,7 @@ def _set_attribute(self, attr, value): ######################################################################################## -# Classes for every HTML element type. If the element type (e.g. "input") clashes with +# Classes for every HTML element. If the element tag name (e.g. "input") clashes with # either a Python keyword or common symbol, then we suffix the class name with an "_" # (e.g. "input_"). From d09963adc94b7efb2608224c94a8c34f0cd14933 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 24 Jul 2024 14:44:39 -0500 Subject: [PATCH 03/34] Commenting. --- pyscript.core/src/stdlib/pyscript/web/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index 5e98a436afc..0bc4cc29528 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -565,7 +565,7 @@ def _set_attribute(self, attr, value): # Classes for every HTML element. If the element tag name (e.g. "input") clashes with # either a Python keyword or common symbol, then we suffix the class name with an "_" -# (e.g. "input_"). +# (e.g. the class for the "input" tag is "input_"). class a(ContainerElement): From 0d3472fc883523f65c60cdfa2ee1689b4554016c Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 24 Jul 2024 14:47:57 -0500 Subject: [PATCH 04/34] Commenting. --- pyscript.core/src/stdlib/pyscript/web/elements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index 0bc4cc29528..dd3ef79cf7f 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -25,8 +25,8 @@ class Element: def from_dom_element(cls, dom_element): """Create an instance of a subclass of `Element` for a DOM element.""" - # Lookup the element class by tag name and for any unknown elements (custom - # tags etc.) use this class (`Element`). + # Lookup the element class by tag name. For any unknown elements (custom + # tags etc.) use *this* class (`Element`). element_cls = ELEMENT_CLASSES_BY_TAG_NAME.get(dom_element.tagName.lower(), cls) return element_cls(dom_element=dom_element) From 18a2f2b823643ff314c1a11015097ee0878f38c6 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 24 Jul 2024 18:28:35 -0500 Subject: [PATCH 05/34] Group dunder methods. --- .../src/stdlib/pyscript/web/elements.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index dd3ef79cf7f..a2fc7c187d2 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -288,6 +288,18 @@ class Options: def __init__(self, element: Element) -> None: self._element = element + def __getitem__(self, key): + return self.options[key] + + 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 add( self, value: Any = None, @@ -321,7 +333,7 @@ def remove(self, item: int) -> None: def clear(self) -> None: """Remove all the options""" - for index in range(len(self)): + while len(self) > 0: self.remove(0) @property @@ -336,18 +348,6 @@ def selected(self): """Return the selected option""" return self.options[self._element._dom_element.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 Style: """A dict-like interface to an element's css style.""" @@ -561,11 +561,9 @@ def _set_attribute(self, attr, value): setattr(el, attr, value) -######################################################################################## - # Classes for every HTML element. If the element tag name (e.g. "input") clashes with # either a Python keyword or common symbol, then we suffix the class name with an "_" -# (e.g. the class for the "input" tag is "input_"). +# (e.g. the class for the "input" element is "input_"). class a(ContainerElement): From 035e2e1584061ab932093590eb11dabc3659cf20 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 26 Jul 2024 09:59:29 -0500 Subject: [PATCH 06/34] Don't cache the element's parent. --- pyscript.core/src/stdlib/pyscript/web/elements.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index a2fc7c187d2..9c079a26d2c 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -41,7 +41,6 @@ def __init__(self, dom_element=None, classes=None, style=None, **kwargs): type(self).__name__.replace("_", "") ) - self._parent = None self._classes = Classes(self) self._style = Style(self) @@ -119,13 +118,10 @@ def classes(self): @property def parent(self): - if self._parent: - return self._parent + if self._dom_element.parentElement is None: + return None - if self._dom_element.parentElement: - self._parent = Element.from_dom_element(self._dom_element.parentElement) - - return self._parent + return Element.from_dom_element(self._dom_element.parentElement) @property def style(self): From 3714711d14d6e503c63d7a5dbb639be8c15f5ee5 Mon Sep 17 00:00:00 2001 From: Martin Date: Sun, 28 Jul 2024 19:49:28 -0600 Subject: [PATCH 07/34] Remove style type check until we decide whether or not to add for classes too. --- .../src/stdlib/pyscript/web/elements.py | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index 9c079a26d2c..c25f1f56e2f 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -47,6 +47,10 @@ def __init__(self, dom_element=None, classes=None, style=None, **kwargs): # Set any specified classes, styles, and DOM properties. self.update(classes=classes, style=style, **kwargs) + def __eq__(self, obj): + """Check for equality by comparing the underlying DOM element.""" + return isinstance(obj, Element) and obj._dom_element == self._dom_element + def __getattr__(self, name): # This allows us to get attributes on the underlying DOM element that clash # with Python keywords or built-ins (e.g. the output element has an @@ -82,30 +86,12 @@ def update(self, classes=None, style=None, **kwargs): if classes: self.classes.add(classes) - if isinstance(style, dict): + if style: self.style.set(**style) - elif style is not None: - raise ValueError( - f"Style should be a dictionary, received {style} " - f"(type {type(style)}) instead." - ) - - self._set_dom_properties(**kwargs) - - def _set_dom_properties(self, **kwargs): - """Set the specified DOM properties. - - Args: - **kwargs: The properties to set - """ for name, value in kwargs.items(): setattr(self, name, value) - def __eq__(self, obj): - """Check for equality by comparing the underlying DOM element.""" - return isinstance(obj, Element) and obj._dom_element == self._dom_element - @property def children(self): return ElementCollection( From 7d6f1e916327d7f736659af4f2dc8ed37fece5c4 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jul 2024 07:23:43 -0600 Subject: [PATCH 08/34] Add ability to register/unregister element classes. --- .../src/stdlib/pyscript/web/elements.py | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index c25f1f56e2f..17024df3136 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -21,13 +21,34 @@ def warn(*args, **kwargs): class Element: + # Lookup table to get an element class by its tag name. + element_classes_by_tag_name = {} + + @classmethod + def register_element_classes(cls, element_classes): + """Register an iterable of element classes.""" + + for element_class in element_classes: + tag_name = element_class.__name__.replace("_", "") + cls.element_classes_by_tag_name[tag_name] = element_class + + @classmethod + def unregister_element_classes(cls, element_classes): + """Unregister an iterable of element classes.""" + + for element_class in element_classes: + tag_name = element_class.__name__.replace("_", "") + cls.element_classes_by_tag_name.pop(tag_name, None) + @classmethod def from_dom_element(cls, dom_element): """Create an instance of a subclass of `Element` for a DOM element.""" # Lookup the element class by tag name. For any unknown elements (custom # tags etc.) use *this* class (`Element`). - element_cls = ELEMENT_CLASSES_BY_TAG_NAME.get(dom_element.tagName.lower(), cls) + element_cls = cls.element_classes_by_tag_name.get( + dom_element.tagName.lower(), cls + ) return element_cls(dom_element=dom_element) @@ -1071,7 +1092,5 @@ class wbr(Element): # fmt: on -# Lookup table to get an element class by its tag name. -ELEMENT_CLASSES_BY_TAG_NAME = { - cls.__name__.replace("_", ""): cls for cls in ELEMENT_CLASSES -} +# Register all the default (aka "built-in") Element classes. +Element.register_element_classes(ELEMENT_CLASSES) From 9e6f65a77f00eda4d5823b4f64a38c22f4703f3e Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jul 2024 09:06:45 -0600 Subject: [PATCH 09/34] Implement __iter__ for container elements. --- .../src/stdlib/pyscript/web/elements.py | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index 17024df3136..b15af014505 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -77,7 +77,7 @@ def __getattr__(self, name): # with Python keywords or built-ins (e.g. the output element has an # attribute `for` which is a Python keyword, so you can access it on the # Element instance via `for_`). - if name.endswith("_"): + if name.endswith("_") and not name.endswith("__"): name = name[:-1] return getattr(self._dom_element, name) @@ -96,7 +96,7 @@ def __setattr__(self, name, value): # with Python keywords or built-ins (e.g. the output element has an # attribute `for` which is a Python keyword, so you can access it on the # Element instance via `for_`). - if name.endswith("_"): + if name.endswith("_") and not name.endswith("__"): name = name[:-1] setattr(self._dom_element, name, value) @@ -402,6 +402,9 @@ def __init__( else: self.innerHTML += child + def __iter__(self): + yield from self.children + class ClassesCollection: def __init__(self, collection: "ElementCollection") -> None: @@ -465,11 +468,10 @@ 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] + return [ + element.style[key] for element in self._collection._elements + ] def __setitem__(self, key, value): for element in self._collection._elements: @@ -518,20 +520,21 @@ def __repr__(self): f"{self._elements}" ) - def __getattr__(self, item): - return self._get_attribute(item) + def __getattr__(self, name): + return [getattr(el, name) for el in self._elements] - def __setattr__(self, key, value): + def __setattr__(self, name, value): # This class overrides `__setattr__` to delegate "public" attributes to the # elements in the collection. BUT, we don't use the usual Python pattern where # we set attributes on the collection itself via `self.__dict__` as that is not # yet supported in our build of MicroPython. Instead, we handle it here by # using super for all "private" attributes (those starting with an underscore). - if key.startswith("_"): - super().__setattr__(key, value) + if name.startswith("_"): + super().__setattr__(name, value) else: - self._set_attribute(key, value) + for el in self._elements: + setattr(el, name, value) @property def children(self): @@ -552,17 +555,6 @@ def find(self, selector): return ElementCollection(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) - # Classes for every HTML element. If the element tag name (e.g. "input") clashes with # either a Python keyword or common symbol, then we suffix the class name with an "_" From e2c0b8891ac62313dae07261fdf3644e2db62fe5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:07:19 +0000 Subject: [PATCH 10/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyscript.core/src/stdlib/pyscript/web/elements.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index b15af014505..0362b67d8cd 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -469,9 +469,7 @@ def __init__(self, collection: "ElementCollection") -> None: self._collection = collection def __getitem__(self, key): - return [ - element.style[key] for element in self._collection._elements - ] + return [element.style[key] for element in self._collection._elements] def __setitem__(self, key, value): for element in self._collection._elements: From 8110fd79d37604dabcd018010f392e478bf950d7 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jul 2024 09:26:40 -0600 Subject: [PATCH 11/34] Minor renaming to make it clear when we have an Element instance vs an actual DOM element. --- .../src/stdlib/pyscript/web/elements.py | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index b15af014505..c1a86103e4f 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -21,7 +21,8 @@ def warn(*args, **kwargs): class Element: - # Lookup table to get an element class by its tag name. + # Lookup table to get an element class by its tag name when wrapping an existing + # DOM element. element_classes_by_tag_name = {} @classmethod @@ -96,27 +97,18 @@ def __setattr__(self, name, value): # with Python keywords or built-ins (e.g. the output element has an # attribute `for` which is a Python keyword, so you can access it on the # Element instance via `for_`). - if name.endswith("_") and not name.endswith("__"): + if name.endswith("_"): name = name[:-1] setattr(self._dom_element, name, value) - def update(self, classes=None, style=None, **kwargs): - """Update the element with the specified classes, styles, and DOM properties.""" - - if classes: - self.classes.add(classes) - - if style: - self.style.set(**style) - - for name, value in kwargs.items(): - setattr(self, name, value) - @property def children(self): return ElementCollection( - [Element.from_dom_element(el) for el in self._dom_element.children] + [ + Element.from_dom_element(dom_element) + for dom_element in self._dom_element.children + ] ) @property @@ -139,8 +131,8 @@ def append(self, child): self._dom_element.appendChild(child._dom_element) elif isinstance(child, ElementCollection): - for el in child: - self._dom_element.appendChild(el._dom_element) + for element in child: + self._dom_element.appendChild(element._dom_element) else: # In this case we know it's not an Element or an ElementCollection, so we @@ -193,6 +185,18 @@ def show_me(self): """Scroll the element into view.""" self._dom_element.scrollIntoView() + def update(self, classes=None, style=None, **kwargs): + """Update the element with the specified classes, styles, and DOM properties.""" + + if classes: + self.classes.add(classes) + + if style: + self.style.set(**style) + + for name, value in kwargs.items(): + setattr(self, name, value) + class Classes: """A set-like interface to an element's `classList`.""" @@ -521,7 +525,7 @@ def __repr__(self): ) def __getattr__(self, name): - return [getattr(el, name) for el in self._elements] + return [getattr(element, name) for element in self._elements] def __setattr__(self, name, value): # This class overrides `__setattr__` to delegate "public" attributes to the @@ -533,8 +537,8 @@ def __setattr__(self, name, value): super().__setattr__(name, value) else: - for el in self._elements: - setattr(el, name, value) + for element in self._elements: + setattr(element, name, value) @property def children(self): From 7a066ec5188e4b609c7fe74fbca5b49df5ec64e8 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jul 2024 11:34:21 -0600 Subject: [PATCH 12/34] remove duplication: added Element.get_tag_name --- .../src/stdlib/pyscript/web/elements.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index 62dc1758e43..bec040a3402 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -25,12 +25,20 @@ class Element: # DOM element. element_classes_by_tag_name = {} + @classmethod + def get_tag_name(cls): + """Return the HTML tag name for the element class.""" + + # For classes that have a trailing underscore (because they clash with a + # Python keyword or built-in), we remove it to get the tag name. + return cls.__name__.replace("_", "") + @classmethod def register_element_classes(cls, element_classes): """Register an iterable of element classes.""" for element_class in element_classes: - tag_name = element_class.__name__.replace("_", "") + tag_name = element_class.get_tag_name() cls.element_classes_by_tag_name[tag_name] = element_class @classmethod @@ -38,12 +46,12 @@ def unregister_element_classes(cls, element_classes): """Unregister an iterable of element classes.""" for element_class in element_classes: - tag_name = element_class.__name__.replace("_", "") + tag_name = element_class.get_tag_name() cls.element_classes_by_tag_name.pop(tag_name, None) @classmethod def from_dom_element(cls, dom_element): - """Create an instance of a subclass of `Element` for a DOM element.""" + """Create an instance of a subclass of `Element` from an existing DOM element.""" # Lookup the element class by tag name. For any unknown elements (custom # tags etc.) use *this* class (`Element`). @@ -60,7 +68,7 @@ def __init__(self, dom_element=None, classes=None, style=None, **kwargs): Otherwise, we are being called to *wrap* an existing DOM element. """ self._dom_element = dom_element or document.createElement( - type(self).__name__.replace("_", "") + type(self).get_tag_name() ) self._classes = Classes(self) From 24c1e37e4ae68cec5cf4ca2b491f9fa03428e5bc Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jul 2024 11:53:41 -0600 Subject: [PATCH 13/34] Commenting. --- .../src/stdlib/pyscript/web/__init__.py | 3 +- .../src/stdlib/pyscript/web/elements.py | 33 +++++++------------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/__init__.py b/pyscript.core/src/stdlib/pyscript/web/__init__.py index 1cdf4164c2e..e45f09a2785 100644 --- a/pyscript.core/src/stdlib/pyscript/web/__init__.py +++ b/pyscript.core/src/stdlib/pyscript/web/__init__.py @@ -10,7 +10,8 @@ def __init__(self): def __getitem__(self, selector): return self.find(selector) - def find(self, selector): + @staticmethod + def find(selector): return ElementCollection( [ Element.from_dom_element(dom_element) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index bec040a3402..c090a301739 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -4,39 +4,29 @@ 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) - from pyscript import document class Element: - # Lookup table to get an element class by its tag name when wrapping an existing + # Lookup table to get an element class by tag name. Used when wrapping an existing # DOM element. element_classes_by_tag_name = {} @classmethod def get_tag_name(cls): - """Return the HTML tag name for the element class.""" + """Return the HTML tag name for the element class. + + For classes that have a trailing underscore (because they clash with a Python + keyword or built-in), we remove it to get the tag name. e.g. for the `input_` + class, the tag name is 'input'. - # For classes that have a trailing underscore (because they clash with a - # Python keyword or built-in), we remove it to get the tag name. + """ return cls.__name__.replace("_", "") @classmethod def register_element_classes(cls, element_classes): """Register an iterable of element classes.""" - for element_class in element_classes: tag_name = element_class.get_tag_name() cls.element_classes_by_tag_name[tag_name] = element_class @@ -44,17 +34,18 @@ def register_element_classes(cls, element_classes): @classmethod def unregister_element_classes(cls, element_classes): """Unregister an iterable of element classes.""" - for element_class in element_classes: tag_name = element_class.get_tag_name() cls.element_classes_by_tag_name.pop(tag_name, None) @classmethod def from_dom_element(cls, dom_element): - """Create an instance of a subclass of `Element` from an existing DOM element.""" + """Wrap an existing DOM element in an instance of a subclass of `Element`. - # Lookup the element class by tag name. For any unknown elements (custom - # tags etc.) use *this* class (`Element`). + We look up the `Element` subclass by the DOM element's tag name. For any unknown + elements (custom tags etc.) use *this* class (`Element`). + + """ element_cls = cls.element_classes_by_tag_name.get( dom_element.tagName.lower(), cls ) From ab1178f69574451d9f7653403e8e997a9521081b Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jul 2024 13:36:09 -0600 Subject: [PATCH 14/34] Allow Element.append to 1) use *args, 2) accept iterables --- .../src/stdlib/pyscript/web/elements.py | 64 +++++++++++-------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index c090a301739..34039cef3a5 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -8,6 +8,15 @@ from pyscript import document +def is_iterable(obj): + try: + iter(obj) + return True + + except TypeError: + return False + + class Element: # Lookup table to get an element class by tag name. Used when wrapping an existing # DOM element. @@ -125,37 +134,42 @@ def parent(self): def style(self): return self._style - def append(self, child): - if isinstance(child, Element): - self._dom_element.appendChild(child._dom_element) + def append(self, *children): + for child in children: + if isinstance(child, Element): + self._dom_element.appendChild(child._dom_element) - elif isinstance(child, ElementCollection): - for element in child: - self._dom_element.appendChild(element._dom_element) + elif isinstance(child, ElementCollection): + for element in child: + self._dom_element.appendChild(element._dom_element) - else: - # In this case we know it's not an Element or an ElementCollection, so we - # guess that it's either a DOM element or NodeList returned via the ffi. - try: - # First, we try to see if it's an element by accessing the 'tagName' - # attribute. - child.tagName - self._dom_element.appendChild(child) + elif is_iterable(child): + for item in child: + self.append(item) - except AttributeError: + else: + # In this case we know it's not an Element or an ElementCollection, so we + # guess that it's either a DOM element or NodeList returned via the ffi. try: - # Ok, it's not an element, so let's see if it's a NodeList by - # accessing the 'length' attribute. - child.length - for element_ in child: - self._dom_element.appendChild(element_) + # First, we try to see if it's an element by accessing the 'tagName' + # attribute. + child.tagName + self._dom_element.appendChild(child) except AttributeError: - # Nope! This is not an element or a NodeList. - raise TypeError( - f'Element "{child}" is a proxy object, "' - f"but not a valid element or a NodeList." - ) + try: + # Ok, it's not an element, so let's see if it's a NodeList by + # accessing the 'length' attribute. + child.length + for element_ in child: + self._dom_element.appendChild(element_) + + except AttributeError: + # Nope! This is not an element or a NodeList. + raise TypeError( + f'Element "{child}" is a proxy object, "' + f"but not a valid element or a NodeList." + ) def clone(self, clone_id=None): """Make a clone of the element (clones the underlying DOM object too).""" From 52779ba6db861f65b2d848197ce541ba3e67021e Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jul 2024 14:02:14 -0600 Subject: [PATCH 15/34] Remove iterable check - inteferes with js proxies. --- pyscript.core/src/stdlib/pyscript/web/elements.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index 34039cef3a5..2d5124e33ce 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -143,10 +143,6 @@ def append(self, *children): for element in child: self._dom_element.appendChild(element._dom_element) - elif is_iterable(child): - for item in child: - self.append(item) - else: # In this case we know it's not an Element or an ElementCollection, so we # guess that it's either a DOM element or NodeList returned via the ffi. From 73ab702c49b410aaed2535978a371c791f67839a Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jul 2024 14:05:20 -0600 Subject: [PATCH 16/34] Don't use *args, so it quacks more like a list ;) --- .../src/stdlib/pyscript/web/elements.py | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index 2d5124e33ce..6930923b76e 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -134,38 +134,38 @@ def parent(self): def style(self): return self._style - def append(self, *children): - for child in children: - if isinstance(child, Element): - self._dom_element.appendChild(child._dom_element) + def append(self, child): + if isinstance(child, Element): + self._dom_element.appendChild(child._dom_element) - elif isinstance(child, ElementCollection): - for element in child: - self._dom_element.appendChild(element._dom_element) + elif isinstance(child, ElementCollection): + for element in child: + self._dom_element.appendChild(element._dom_element) - else: - # In this case we know it's not an Element or an ElementCollection, so we - # guess that it's either a DOM element or NodeList returned via the ffi. + else: + # In this case we know it's not an Element or an ElementCollection, so + # we guess that it's either a DOM element or NodeList returned via the + # ffi. + try: + # First, we try to see if it's an element by accessing the 'tagName' + # attribute. + child.tagName + self._dom_element.appendChild(child) + + except AttributeError: try: - # First, we try to see if it's an element by accessing the 'tagName' - # attribute. - child.tagName - self._dom_element.appendChild(child) + # Ok, it's not an element, so let's see if it's a NodeList by + # accessing the 'length' attribute. + child.length + for element_ in child: + self._dom_element.appendChild(element_) except AttributeError: - try: - # Ok, it's not an element, so let's see if it's a NodeList by - # accessing the 'length' attribute. - child.length - for element_ in child: - self._dom_element.appendChild(element_) - - except AttributeError: - # Nope! This is not an element or a NodeList. - raise TypeError( - f'Element "{child}" is a proxy object, "' - f"but not a valid element or a NodeList." - ) + # Nope! This is not an element or a NodeList. + raise TypeError( + f'Element "{child}" is a proxy object, "' + f"but not a valid element or a NodeList." + ) def clone(self, clone_id=None): """Make a clone of the element (clones the underlying DOM object too).""" From 395c7856c98a96fb25221e908df32746a3350cab Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jul 2024 15:52:16 -0600 Subject: [PATCH 17/34] Element.append take 2 :) --- .../src/stdlib/pyscript/web/elements.py | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index 6930923b76e..0ad94969904 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -134,38 +134,45 @@ def parent(self): def style(self): return self._style - def append(self, child): - if isinstance(child, Element): - self._dom_element.appendChild(child._dom_element) + def append(self, *children): + for child in children: + if isinstance(child, Element): + self._dom_element.appendChild(child._dom_element) - elif isinstance(child, ElementCollection): - for element in child: - self._dom_element.appendChild(element._dom_element) + elif isinstance(child, ElementCollection): + for element in child: + self._dom_element.appendChild(element._dom_element) - else: - # In this case we know it's not an Element or an ElementCollection, so - # we guess that it's either a DOM element or NodeList returned via the - # ffi. - try: - # First, we try to see if it's an element by accessing the 'tagName' - # attribute. - child.tagName - self._dom_element.appendChild(child) + # We don't use a check for iterables here as it will match JS proxies for + # NodeList. + elif isinstance(child, list) or isinstance(child, tuple): + for item in child: + self.append(item) - except AttributeError: + else: + # In this case we know it's not an Element or an ElementCollection, so + # we guess that it's either a DOM element or NodeList returned via the + # ffi. try: - # Ok, it's not an element, so let's see if it's a NodeList by - # accessing the 'length' attribute. - child.length - for element_ in child: - self._dom_element.appendChild(element_) + # First, we try to see if it's an element by accessing the 'tagName' + # attribute. + child.tagName + self._dom_element.appendChild(child) except AttributeError: - # Nope! This is not an element or a NodeList. - raise TypeError( - f'Element "{child}" is a proxy object, "' - f"but not a valid element or a NodeList." - ) + try: + # Ok, it's not an element, so let's see if it's a NodeList by + # accessing the 'length' attribute. + child.length + for element_ in child: + self._dom_element.appendChild(element_) + + except AttributeError: + # Nope! This is not an element or a NodeList. + raise TypeError( + f'Element "{child}" is a proxy object, "' + f"but not a valid element or a NodeList." + ) def clone(self, clone_id=None): """Make a clone of the element (clones the underlying DOM object too).""" From 3725f0cb8c029d445d766a4f297fe9b083d36fc0 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jul 2024 16:04:34 -0600 Subject: [PATCH 18/34] Remove unused code. --- pyscript.core/src/stdlib/pyscript/web/elements.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web/elements.py index 0ad94969904..910fa6454d4 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web/elements.py @@ -8,15 +8,6 @@ from pyscript import document -def is_iterable(obj): - try: - iter(obj) - return True - - except TypeError: - return False - - class Element: # Lookup table to get an element class by tag name. Used when wrapping an existing # DOM element. @@ -143,7 +134,8 @@ def append(self, *children): for element in child: self._dom_element.appendChild(element._dom_element) - # We don't use a check for iterables here as it will match JS proxies for + # We check for list/tuple here and NOT for any iterable as it will match + # a JS Nodelist which is handled explicitly below. # NodeList. elif isinstance(child, list) or isinstance(child, tuple): for item in child: From fe7f70f92453d725db69464fd9639d5753f3dfd3 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 31 Jul 2024 08:14:11 -0600 Subject: [PATCH 19/34] Move to web.py with a page object! --- .../src/stdlib/pyscript/event_handling.py | 2 +- .../pyscript/{web/elements.py => web.py} | 61 +++++++--- .../src/stdlib/pyscript/web/__init__.py | 23 ---- .../test/pyscript_dom/tests/test_dom.py | 104 +++++++++--------- pyscript.core/tests/integration/test_pyweb.py | 30 ++--- pyscript.core/types/stdlib/pyscript.d.ts | 5 +- 6 files changed, 110 insertions(+), 115 deletions(-) rename pyscript.core/src/stdlib/pyscript/{web/elements.py => web.py} (95%) delete mode 100644 pyscript.core/src/stdlib/pyscript/web/__init__.py diff --git a/pyscript.core/src/stdlib/pyscript/event_handling.py b/pyscript.core/src/stdlib/pyscript/event_handling.py index e3d388d9076..be133e4eeeb 100644 --- a/pyscript.core/src/stdlib/pyscript/event_handling.py +++ b/pyscript.core/src/stdlib/pyscript/event_handling.py @@ -20,7 +20,7 @@ def when(event_type=None, selector=None): def decorator(func): - from pyscript.web.elements import Element, ElementCollection + from pyscript.web import Element, ElementCollection if isinstance(selector, str): elements = document.querySelectorAll(selector) diff --git a/pyscript.core/src/stdlib/pyscript/web/elements.py b/pyscript.core/src/stdlib/pyscript/web.py similarity index 95% rename from pyscript.core/src/stdlib/pyscript/web/elements.py rename to pyscript.core/src/stdlib/pyscript/web.py index 910fa6454d4..cd6a38c8894 100644 --- a/pyscript.core/src/stdlib/pyscript/web/elements.py +++ b/pyscript.core/src/stdlib/pyscript/web.py @@ -6,6 +6,17 @@ from pyscript import document +from pyscript import when # noqa: impoprted to expose via this module. + + +def wrap_dom_element(dom_element): + """Wrap an existing DOM element in an instance of a subclass of `Element`. + + This is just a convenience function to avoid having to import the `Element` class + and use a class method. + """ + + return Element.wrap_dom_element(dom_element) class Element: @@ -39,7 +50,7 @@ def unregister_element_classes(cls, element_classes): cls.element_classes_by_tag_name.pop(tag_name, None) @classmethod - def from_dom_element(cls, dom_element): + def wrap_dom_element(cls, dom_element): """Wrap an existing DOM element in an instance of a subclass of `Element`. We look up the `Element` subclass by the DOM element's tag name. For any unknown @@ -103,12 +114,7 @@ def __setattr__(self, name, value): @property def children(self): - return ElementCollection( - [ - Element.from_dom_element(dom_element) - for dom_element in self._dom_element.children - ] - ) + return ElementCollection.wrap_dom_elements(self._dom_element.children) @property def classes(self): @@ -119,7 +125,7 @@ def parent(self): if self._dom_element.parentElement is None: return None - return Element.from_dom_element(self._dom_element.parentElement) + return Element.wrap_dom_element(self._dom_element.parentElement) @property def style(self): @@ -168,7 +174,7 @@ def append(self, *children): def clone(self, clone_id=None): """Make a clone of the element (clones the underlying DOM object too).""" - clone = Element.from_dom_element(self._dom_element.cloneNode(True)) + clone = Element.wrap_dom_element(self._dom_element.cloneNode(True)) clone.id = clone_id return clone @@ -182,11 +188,8 @@ def find(self, selector): Returns: ElementCollection: A collection of elements matching the selector """ - return ElementCollection( - [ - Element.from_dom_element(dom_element) - for dom_element in self._dom_element.querySelectorAll(selector) - ] + return ElementCollection.wrap_dom_elements( + self._dom_element.querySelectorAll(selector) ) def show_me(self): @@ -355,7 +358,7 @@ def clear(self) -> None: def options(self): """Return the list of options""" return [ - Element.from_dom_element(opt) for opt in self._element._dom_element.options + Element.wrap_dom_element(opt) for opt in self._element._dom_element.options ] @property @@ -496,6 +499,14 @@ def remove(self, key): class ElementCollection: + @classmethod + def wrap_dom_elements(cls, dom_elements): + """Wrap an iterable of dom_elements in an `ElementCollection`.""" + + return cls( + [Element.wrap_dom_element(dom_element) for dom_element in dom_elements] + ) + def __init__(self, elements: [Element]) -> None: self._elements = elements self._classes = ClassesCollection(self) @@ -614,7 +625,6 @@ class blockquote(ContainerElement): class body(ContainerElement): """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body""" - class br(Element): """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br""" @@ -1096,3 +1106,22 @@ class wbr(Element): # Register all the default (aka "built-in") Element classes. Element.register_element_classes(ELEMENT_CLASSES) + + +class Page: + """Wraps the `document` object.""" + + def __init__(self): + self.body = Element.wrap_dom_element(document.body) + self.head = Element.wrap_dom_element(document.head) + + @staticmethod + def find(selector): + """Find all elements that match the specified selector. + + Return the results as a possibly `ElementCollection`. + """ + return ElementCollection.wrap_dom_elements(document.querySelectorAll(selector)) + + +page = Page() diff --git a/pyscript.core/src/stdlib/pyscript/web/__init__.py b/pyscript.core/src/stdlib/pyscript/web/__init__.py deleted file mode 100644 index e45f09a2785..00000000000 --- a/pyscript.core/src/stdlib/pyscript/web/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from pyscript import document -from pyscript.web.elements import Element, ElementCollection - - -class DOM: - def __init__(self): - self.body = Element.from_dom_element(document.body) - self.head = Element.from_dom_element(document.head) - - def __getitem__(self, selector): - return self.find(selector) - - @staticmethod - def find(selector): - return ElementCollection( - [ - Element.from_dom_element(dom_element) - for dom_element in document.querySelectorAll(selector) - ] - ) - - -dom = DOM() diff --git a/pyscript.core/test/pyscript_dom/tests/test_dom.py b/pyscript.core/test/pyscript_dom/tests/test_dom.py index 9285036edf4..22250fe6fa4 100644 --- a/pyscript.core/test/pyscript_dom/tests/test_dom.py +++ b/pyscript.core/test/pyscript_dom/tests/test_dom.py @@ -1,13 +1,11 @@ from pyscript import document, when -from pyscript.web import dom -from pyscript.web import elements as el -from pyscript.web.elements import ElementCollection +from pyscript.web import Element, ElementCollection, div, p, page, Element class TestDocument: def test__element(self): - assert dom.body._dom_element == document.body - assert dom.head._dom_element == document.head + assert page.body._dom_element == document.body + assert page.head._dom_element == document.head def test_getitem_by_id(): @@ -16,13 +14,13 @@ def test_getitem_by_id(): txt = "You found test_id_selector" selector = f"#{id_}" # EXPECT the element to be found by id - result = dom.find(selector) + result = page.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.innerHTML == txt # EXPECT the results to be of the right types - assert isinstance(div, el.Element) + assert isinstance(div, Element) assert isinstance(result, ElementCollection) @@ -33,8 +31,7 @@ def test_getitem_by_class(): "test_selector_w_children_child_1", ] expected_class = "a-test-class" - result = dom.find(f".{expected_class}") - div = result[0] + result = page.find(f".{expected_class}") # EXPECT to find exact number of elements with the class in the page (== 3) assert len(result) == 3 @@ -44,7 +41,7 @@ def test_getitem_by_class(): def test_read_n_write_collection_elements(): - elements = dom.find(".multi-elems") + elements = page.find(".multi-elems") for element in elements: assert element.innerHTML == f"Content {element.id.replace('#', '')}" @@ -59,15 +56,15 @@ class TestElement: def test_query(self): # GIVEN an existing element on the page, with at least 1 child element id_ = "test_selector_w_children" - parent_div = dom.find(f"#{id_}")[0] + parent_div = page.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 el.Element - assert isinstance(div, el.Element) + # EXPECT the new element to be an Element + assert isinstance(div, Element) # EXPECT the div attributes to be == to how they are configured in the page assert div.innerHTML == "Child 1" assert div.id == "test_selector_w_children_child_1" @@ -76,8 +73,8 @@ def test_equality(self): # GIVEN 2 different Elements pointing to the same underlying element id_ = "test_id_selector" selector = f"#{id_}" - div = dom.find(selector)[0] - div2 = dom.find(selector)[0] + div = page.find(selector)[0] + div2 = page.find(selector)[0] # EXPECT them to be equal assert div == div2 @@ -92,27 +89,27 @@ def test_equality(self): def test_append_element(self): id_ = "element-append-tests" - div = dom.find(f"#{id_}")[0] + div = page.find(f"#{id_}")[0] len_children_before = len(div.children) - new_el = el.p("new element") + new_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_dom_element_element(self): id_ = "element-append-tests" - div = dom.find(f"#{id_}")[0] + div = page.find(f"#{id_}")[0] len_children_before = len(div.children) - new_el = el.p("new element") + new_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 = dom.find(f"#{id_}")[0] + div = page.find(f"#{id_}")[0] len_children_before = len(div.children) - collection = dom.find(".collection") + collection = page.find(".collection") div.append(collection) assert len(div.children) == len_children_before + len(collection) @@ -122,16 +119,16 @@ def test_append_collection(self): def test_read_classes(self): id_ = "test_class_selector" expected_class = "a-test-class" - div = dom.find(f"#{id_}")[0] + div = page.find(f"#{id_}")[0] assert div.classes == [expected_class] def test_add_remove_class(self): id_ = "div-no-classes" classname = "tester-class" - div = dom.find(f"#{id_}")[0] + div = page.find(f"#{id_}")[0] assert not div.classes div.classes.add(classname) - same_div = dom.find(f"#{id_}")[0] + same_div = page.find(f"#{id_}")[0] assert div.classes == [classname] == same_div.classes div.classes.remove(classname) assert div.classes == [] == same_div.classes @@ -139,7 +136,7 @@ def test_add_remove_class(self): def test_when_decorator(self): called = False - just_a_button = dom.find("#a-test-button")[0] + just_a_button = page.find("#a-test-button")[0] @when("click", just_a_button) def on_click(event): @@ -155,7 +152,7 @@ def on_click(event): def test_inner_html_attribute(self): # GIVEN an existing element on the page with a known empty text content - div = dom.find("#element_attribute_tests")[0] + div = page.find("#element_attribute_tests")[0] # WHEN we set the html attribute div.innerHTML = "New Content" @@ -167,7 +164,7 @@ def test_inner_html_attribute(self): def test_text_attribute(self): # GIVEN an existing element on the page with a known empty text content - div = dom.find("#element_attribute_tests")[0] + div = page.find("#element_attribute_tests")[0] # WHEN we set the html attribute div.textContent = "New Content" @@ -184,12 +181,12 @@ def test_text_attribute(self): class TestCollection: def test_iter_eq_children(self): - elements = dom.find(".multi-elems") + elements = page.find(".multi-elems") assert [el for el in elements] == [el for el in elements.children] assert len(elements) == 3 def test_slices(self): - elements = dom.find(".multi-elems") + elements = page.find(".multi-elems") assert elements[0] _slice = elements[:2] assert len(_slice) == 2 @@ -199,26 +196,26 @@ def test_slices(self): def test_style_rule(self): selector = ".multi-elems" - elements = dom.find(selector) + elements = page.find(selector) for el in elements: assert el.style["background-color"] != "red" elements.style["background-color"] = "red" - for i, el in enumerate(dom.find(selector)): + for i, el in enumerate(page.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(dom.find(selector)): + for i, el in enumerate(page.find(selector)): assert el.style["background-color"] != "red" assert elements[i].style["background-color"] != "red" def test_when_decorator(self): called = False - buttons_collection = dom.find("button") + buttons_collection = page.find("button") @when("click", buttons_collection) def on_click(event): @@ -236,34 +233,35 @@ def on_click(event): class TestCreation: def test_create_document_element(self): - # TODO: This test should probably be removed since it's testing the elements module - new_el = el.div("new element") + # TODO: This test should probably be removed since it's testing the elements + # module. + new_el = div("new element") new_el.id = "new_el_id" - assert isinstance(new_el, el.Element) + assert isinstance(new_el, Element) assert new_el._dom_element.tagName == "DIV" # EXPECT the new element to be associated with the document - assert new_el.parent == None - dom.body.append(new_el) + assert new_el.parent is None + page.body.append(new_el) - assert dom.find("#new_el_id")[0].parent == dom.body + assert page.find("#new_el_id")[0].parent == page.body def test_create_element_child(self): selector = "#element-creation-test" - parent_div = dom.find(selector)[0] + parent_div = page.find(selector)[0] # Creating an element from another element automatically creates that element # as a child of the original element - new_el = el.p( + new_el = p( "a div", classes=["code-description"], innerHTML="Ciao PyScripters!" ) parent_div.append(new_el) - assert isinstance(new_el, el.Element) + assert isinstance(new_el, Element) assert new_el._dom_element.tagName == "P" # EXPECT the new element to be associated with the document assert new_el.parent == parent_div - assert dom.find(selector)[0].children[0] == new_el + assert page.find(selector)[0].children[0] == new_el class TestInput: @@ -277,7 +275,7 @@ class TestInput: def test_value(self): for id_ in self.input_ids: expected_type = id_.split("_")[-1] - result = dom.find(f"#{id_}") + result = page.find(f"#{id_}") input_el = result[0] assert input_el._dom_element.type == expected_type assert input_el.value == f"Content {id_}" == input_el._dom_element.value @@ -295,7 +293,7 @@ def test_value(self): def test_set_value_collection(self): for id_ in self.input_ids: - input_el = dom.find(f"#{id_}") + input_el = page.find(f"#{id_}") assert input_el.value[0] == f"Content {id_}" == input_el[0].value @@ -308,30 +306,30 @@ def test_set_value_collection(self): # actually on the class. Maybe a job for __setattr__? # # def test_element_without_value(self): - # result = dom.find(f"#tests-terminal"][0] + # result = page.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"] + # result = page.find(f"#tests-terminal"] # with pytest.raises(AttributeError): # result.value = "some value" class TestSelect: def test_select_options_iter(self): - select = dom.find(f"#test_select_element_w_options")[0] + select = page.find(f"#test_select_element_w_options")[0] for i, option in enumerate(select.options, 1): assert option.value == f"{i}" assert option.innerHTML == f"Option {i}" def test_select_options_len(self): - select = dom.find(f"#test_select_element_w_options")[0] + select = page.find(f"#test_select_element_w_options")[0] assert len(select.options) == 2 def test_select_options_clear(self): - select = dom.find(f"#test_select_element_to_clear")[0] + select = page.find(f"#test_select_element_to_clear")[0] assert len(select.options) == 3 select.options.clear() @@ -340,7 +338,7 @@ def test_select_options_clear(self): def test_select_element_add(self): # GIVEN the existing select element with no options - select = dom.find(f"#test_select_element")[0] + select = page.find(f"#test_select_element")[0] # EXPECT the select element to have no options assert len(select.options) == 0 @@ -431,7 +429,7 @@ def test_select_element_add(self): def test_select_options_remove(self): # GIVEN the existing select element with 3 options - select = dom.find(f"#test_select_element_to_remove")[0] + select = page.find(f"#test_select_element_to_remove")[0] # EXPECT the select element to have 3 options assert len(select.options) == 4 @@ -453,7 +451,7 @@ def test_select_options_remove(self): def test_select_get_selected_option(self): # GIVEN the existing select element with one selected option - select = dom.find(f"#test_select_element_w_options")[0] + select = page.find(f"#test_select_element_w_options")[0] # WHEN we get the selected option selected_option = select.options.selected diff --git a/pyscript.core/tests/integration/test_pyweb.py b/pyscript.core/tests/integration/test_pyweb.py index feb7f79ed5d..60ac39264f4 100644 --- a/pyscript.core/tests/integration/test_pyweb.py +++ b/pyscript.core/tests/integration/test_pyweb.py @@ -101,11 +101,9 @@ def parse_value(v): code_ = f""" from pyscript import when """ self.pyscript_run(code_) @@ -620,13 +618,12 @@ def test_append_py_element(self, interpreter): code_ = f""" from pyscript import when """ self.pyscript_run(code_) @@ -664,14 +661,13 @@ def test_append_proxy_element(self, interpreter): from pyscript import when """ self.pyscript_run(code_) @@ -709,15 +705,14 @@ def test_append_py_elementcollection(self, interpreter): code_ = f""" from pyscript import when """ self.pyscript_run(code_) @@ -765,20 +760,19 @@ def test_append_js_element_nodelist(self, interpreter): from pyscript import when """ self.pyscript_run(code_) diff --git a/pyscript.core/types/stdlib/pyscript.d.ts b/pyscript.core/types/stdlib/pyscript.d.ts index 1c52b3dd43f..730c3b85ff9 100644 --- a/pyscript.core/types/stdlib/pyscript.d.ts +++ b/pyscript.core/types/stdlib/pyscript.d.ts @@ -9,10 +9,7 @@ declare namespace _default { "magic_js.py": string; "storage.py": string; "util.py": string; - web: { - "__init__.py": string; - "elements.py": string; - }; + "web.py": string; "websocket.py": string; "workers.py": string; }; From bbee95a9b21b135610917c378becf7fb675b421a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:14:43 +0000 Subject: [PATCH 20/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyscript.core/src/stdlib/pyscript/web.py | 3 ++- pyscript.core/test/pyscript_dom/tests/test_dom.py | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web.py b/pyscript.core/src/stdlib/pyscript/web.py index cd6a38c8894..04cb02fb71d 100644 --- a/pyscript.core/src/stdlib/pyscript/web.py +++ b/pyscript.core/src/stdlib/pyscript/web.py @@ -5,8 +5,8 @@ Any = "Any" -from pyscript import document from pyscript import when # noqa: impoprted to expose via this module. +from pyscript import document def wrap_dom_element(dom_element): @@ -625,6 +625,7 @@ class blockquote(ContainerElement): class body(ContainerElement): """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body""" + class br(Element): """Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br""" diff --git a/pyscript.core/test/pyscript_dom/tests/test_dom.py b/pyscript.core/test/pyscript_dom/tests/test_dom.py index 22250fe6fa4..b1cf2a88da9 100644 --- a/pyscript.core/test/pyscript_dom/tests/test_dom.py +++ b/pyscript.core/test/pyscript_dom/tests/test_dom.py @@ -1,5 +1,5 @@ from pyscript import document, when -from pyscript.web import Element, ElementCollection, div, p, page, Element +from pyscript.web import Element, ElementCollection, div, p, page class TestDocument: @@ -251,9 +251,7 @@ def test_create_element_child(self): # Creating an element from another element automatically creates that element # as a child of the original element - new_el = p( - "a div", classes=["code-description"], innerHTML="Ciao PyScripters!" - ) + new_el = p("a div", classes=["code-description"], innerHTML="Ciao PyScripters!") parent_div.append(new_el) assert isinstance(new_el, Element) From 6e02a84d4aa74b0ff44b2a0172eee0d50b4b1c7a Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 31 Jul 2024 08:34:16 -0600 Subject: [PATCH 21/34] Added 'page.title' too :) --- pyscript.core/src/stdlib/pyscript/web.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web.py b/pyscript.core/src/stdlib/pyscript/web.py index cd6a38c8894..a91bc7a7355 100644 --- a/pyscript.core/src/stdlib/pyscript/web.py +++ b/pyscript.core/src/stdlib/pyscript/web.py @@ -1109,14 +1109,28 @@ class wbr(Element): class Page: - """Wraps the `document` object.""" + """Represents the whole page.""" def __init__(self): self.body = Element.wrap_dom_element(document.body) self.head = Element.wrap_dom_element(document.head) - @staticmethod - def find(selector): + @property + def title(self): + """Return the page title.""" + return document.title + + @title.setter + def title(self, value): + """Set the page title.""" + document.title = value + + def append(self, *children): + """Shortcut for `page.body.append`.""" + + self.body.append(*children) + + def find(self, selector): # NOQA """Find all elements that match the specified selector. Return the results as a possibly `ElementCollection`. From ef94e298577ef63c4633ce9624485d366d342e8f Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 31 Jul 2024 10:25:01 -0600 Subject: [PATCH 22/34] Add __getitem__ as a shortcut for page.find --- pyscript.core/src/stdlib/pyscript/web.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyscript.core/src/stdlib/pyscript/web.py b/pyscript.core/src/stdlib/pyscript/web.py index e795cc9ebad..237fe54aeff 100644 --- a/pyscript.core/src/stdlib/pyscript/web.py +++ b/pyscript.core/src/stdlib/pyscript/web.py @@ -1116,6 +1116,10 @@ def __init__(self): self.body = Element.wrap_dom_element(document.body) self.head = Element.wrap_dom_element(document.head) + def __getitem__(self, selector): + """Shortcut for `page.find`.""" + return self.find(selector) + @property def title(self): """Return the page title.""" From b9b239b764d3d1b0aae2240337a0c87fa5da6fe4 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 31 Jul 2024 11:03:01 -0600 Subject: [PATCH 23/34] Add Element.__getitem__ to be consistent --- pyscript.core/src/stdlib/pyscript/web.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyscript.core/src/stdlib/pyscript/web.py b/pyscript.core/src/stdlib/pyscript/web.py index 237fe54aeff..83c93982622 100644 --- a/pyscript.core/src/stdlib/pyscript/web.py +++ b/pyscript.core/src/stdlib/pyscript/web.py @@ -83,6 +83,10 @@ def __eq__(self, obj): """Check for equality by comparing the underlying DOM element.""" return isinstance(obj, Element) and obj._dom_element == self._dom_element + def __getitem__(self, selector): + """Shortcut for `element.find`.""" + return self.find(selector) + def __getattr__(self, name): # This allows us to get attributes on the underlying DOM element that clash # with Python keywords or built-ins (e.g. the output element has an From ef4a18563bd58a019f0d7db3cdc418bf8e24fdcb Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 31 Jul 2024 11:55:51 -0600 Subject: [PATCH 24/34] Make __getitem__ consistent for Page, Element and ElementCollection. --- pyscript.core/src/stdlib/pyscript/web.py | 40 ++++++++++++++++++------ 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web.py b/pyscript.core/src/stdlib/pyscript/web.py index 83c93982622..d6c76885b43 100644 --- a/pyscript.core/src/stdlib/pyscript/web.py +++ b/pyscript.core/src/stdlib/pyscript/web.py @@ -83,9 +83,16 @@ def __eq__(self, obj): """Check for equality by comparing the underlying DOM element.""" return isinstance(obj, Element) and obj._dom_element == self._dom_element - def __getitem__(self, selector): - """Shortcut for `element.find`.""" - return self.find(selector) + def __getitem__(self, key): + """Get an item within the element's children. + + If `key` is an integer or a slice we use it to index/slice the element's + children. Otherwise, we use `key` as a query selector. + """ + if isinstance(key, int) or isinstance(key, slice): + return self.children[key] + + return self.find(key) def __getattr__(self, name): # This allows us to get attributes on the underlying DOM element that clash @@ -521,16 +528,17 @@ def __eq__(self, obj): return isinstance(obj, ElementCollection) and obj._elements == self._elements def __getitem__(self, key): - # If it's an integer we use it to access the elements in the collection + """Get an item in the collection. + + If `key` is an integer or a slice we use it to index/slice the collection. + Otherwise, we use `key` as a query selector. + """ 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 query selector. return self.find(key) def __iter__(self): @@ -1120,9 +1128,21 @@ def __init__(self): self.body = Element.wrap_dom_element(document.body) self.head = Element.wrap_dom_element(document.head) - def __getitem__(self, selector): - """Shortcut for `page.find`.""" - return self.find(selector) + def __getitem__(self, key): + """Get an item on the page. + + If `key` is an integer or a slice we use it to index/slice the document's + children (not *that* useful, as a document has a single child, the + element, but...). Otherwise, we use `key` as a query selector. + """ + if isinstance(key, int) or isinstance(key, slice): + return self.children[key] + + return self.find(key) + + @property + def children(self): + return ElementCollection.wrap_dom_elements(document.children) @property def title(self): From 0d9b1fbff20dfbf6919d771fc5c2470261aeefc2 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 31 Jul 2024 12:05:39 -0600 Subject: [PATCH 25/34] Docstringing. --- pyscript.core/src/stdlib/pyscript/web.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web.py b/pyscript.core/src/stdlib/pyscript/web.py index d6c76885b43..e09484d797f 100644 --- a/pyscript.core/src/stdlib/pyscript/web.py +++ b/pyscript.core/src/stdlib/pyscript/web.py @@ -125,6 +125,7 @@ def __setattr__(self, name, value): @property def children(self): + """Return the element's children as an `ElementCollection`.""" return ElementCollection.wrap_dom_elements(self._dom_element.children) @property @@ -133,6 +134,7 @@ def classes(self): @property def parent(self): + """Return the element's `parent.""" if self._dom_element.parentElement is None: return None @@ -143,6 +145,7 @@ def style(self): return self._style def append(self, *children): + """Append the specified children to the element.""" for child in children: if isinstance(child, Element): self._dom_element.appendChild(child._dom_element) @@ -1131,8 +1134,8 @@ def __init__(self): def __getitem__(self, key): """Get an item on the page. - If `key` is an integer or a slice we use it to index/slice the document's - children (not *that* useful, as a document has a single child, the + If `key` is an integer or a slice we use it to index/slice the pages's + children (not *that* useful, as the page always has a single child, the element, but...). Otherwise, we use `key` as a query selector. """ if isinstance(key, int) or isinstance(key, slice): @@ -1142,6 +1145,10 @@ def __getitem__(self, key): @property def children(self): + """Return the page's children as an `ElementCollection`. + + The page always has a single child, the element. + """ return ElementCollection.wrap_dom_elements(document.children) @property From 1eed03acc852ce0b4e5ddb7da05f9e1da6d7fe75 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 31 Jul 2024 12:14:51 -0600 Subject: [PATCH 26/34] Docstringing. --- pyscript.core/src/stdlib/pyscript/web.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web.py b/pyscript.core/src/stdlib/pyscript/web.py index e09484d797f..f92b699aff5 100644 --- a/pyscript.core/src/stdlib/pyscript/web.py +++ b/pyscript.core/src/stdlib/pyscript/web.py @@ -5,7 +5,7 @@ Any = "Any" -from pyscript import when # noqa: impoprted to expose via this module. +from pyscript import when # noqa: imported to expose `when` via this module. from pyscript import document @@ -13,7 +13,7 @@ def wrap_dom_element(dom_element): """Wrap an existing DOM element in an instance of a subclass of `Element`. This is just a convenience function to avoid having to import the `Element` class - and use a class method. + and use the class method. """ return Element.wrap_dom_element(dom_element) @@ -1125,7 +1125,7 @@ class wbr(Element): class Page: - """Represents the whole page.""" + """Represents the whole page (aka document).""" def __init__(self): self.body = Element.wrap_dom_element(document.body) @@ -1134,9 +1134,10 @@ def __init__(self): def __getitem__(self, key): """Get an item on the page. - If `key` is an integer or a slice we use it to index/slice the pages's - children (not *that* useful, as the page always has a single child, the - element, but...). Otherwise, we use `key` as a query selector. + If `key` is an integer or a slice we use it to index/slice the page's children + (not *that* useful, as the page always has a single child, the element + but consistent with `Element` and `ElementCollection`). Otherwise, we use `key` + as a query selector. """ if isinstance(key, int) or isinstance(key, slice): return self.children[key] From 47bbf32cd64beec2a37d1db1b42c669c35946733 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 31 Jul 2024 14:11:13 -0600 Subject: [PATCH 27/34] Docstringing/commenting. --- pyscript.core/src/stdlib/pyscript/web.py | 195 +++++++++++------------ 1 file changed, 93 insertions(+), 102 deletions(-) diff --git a/pyscript.core/src/stdlib/pyscript/web.py b/pyscript.core/src/stdlib/pyscript/web.py index f92b699aff5..4847b8f204e 100644 --- a/pyscript.core/src/stdlib/pyscript/web.py +++ b/pyscript.core/src/stdlib/pyscript/web.py @@ -1,36 +1,33 @@ -try: - from typing import Any +"""Lightweight Python access to the DOM and HTML elements.""" -except ImportError: - Any = "Any" - -from pyscript import when # noqa: imported to expose `when` via this module. -from pyscript import document +# `when` is not used in this module. It is imported here save the user an additional +# import (i.e. they can get what they need from `pyscript.web`). +from pyscript import document, when # NOQA def wrap_dom_element(dom_element): """Wrap an existing DOM element in an instance of a subclass of `Element`. This is just a convenience function to avoid having to import the `Element` class - and use the class method. + and use its class method. """ return Element.wrap_dom_element(dom_element) class Element: - # Lookup table to get an element class by tag name. Used when wrapping an existing - # DOM element. + # A lookup table to get an `Element` subclass by tag name. Used when wrapping an + # existing DOM element. element_classes_by_tag_name = {} @classmethod def get_tag_name(cls): - """Return the HTML tag name for the element class. + """Return the HTML tag name for the class. For classes that have a trailing underscore (because they clash with a Python keyword or built-in), we remove it to get the tag name. e.g. for the `input_` - class, the tag name is 'input'. + class, the tag name is `input`. """ return cls.__name__.replace("_", "") @@ -130,11 +127,12 @@ def children(self): @property def classes(self): + """Return the element's `classList` as a `Classes` instance.""" return self._classes @property def parent(self): - """Return the element's `parent.""" + """Return the element's `parent `Element`.""" if self._dom_element.parentElement is None: return None @@ -142,24 +140,25 @@ def parent(self): @property def style(self): + """Return the element's `style` attribute as a `Style` instance.""" return self._style - def append(self, *children): - """Append the specified children to the element.""" - for child in children: - if isinstance(child, Element): - self._dom_element.appendChild(child._dom_element) + def append(self, *items): + """Append the specified items to the element.""" + for item in items: + if isinstance(item, Element): + self._dom_element.appendChild(item._dom_element) - elif isinstance(child, ElementCollection): - for element in child: + elif isinstance(item, ElementCollection): + for element in item: self._dom_element.appendChild(element._dom_element) # We check for list/tuple here and NOT for any iterable as it will match # a JS Nodelist which is handled explicitly below. # NodeList. - elif isinstance(child, list) or isinstance(child, tuple): - for item in child: - self.append(item) + elif isinstance(item, list) or isinstance(item, tuple): + for child in item: + self.append(child) else: # In this case we know it's not an Element or an ElementCollection, so @@ -168,21 +167,21 @@ def append(self, *children): try: # First, we try to see if it's an element by accessing the 'tagName' # attribute. - child.tagName - self._dom_element.appendChild(child) + item.tagName + self._dom_element.appendChild(item) except AttributeError: try: # Ok, it's not an element, so let's see if it's a NodeList by # accessing the 'length' attribute. - child.length - for element_ in child: + item.length + for element_ in item: self._dom_element.appendChild(element_) except AttributeError: # Nope! This is not an element or a NodeList. raise TypeError( - f'Element "{child}" is a proxy object, "' + f'Element "{item}" is a proxy object, "' f"but not a valid element or a NodeList." ) @@ -193,14 +192,9 @@ def clone(self, clone_id=None): return clone 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 + """Find all elements that match the specified selector. - Returns: - ElementCollection: A collection of elements matching the selector + Return the results as a (possibly empty) `ElementCollection`. """ return ElementCollection.wrap_dom_elements( self._dom_element.querySelectorAll(selector) @@ -262,6 +256,7 @@ def __str__(self): return " ".join(self._class_list) def add(self, *class_names): + """Add one or more classes to the element's `classList`.""" for class_name in class_names: if isinstance(class_name, list): for item in class_name: @@ -271,9 +266,11 @@ def add(self, *class_names): self._class_list.add(class_name) def contains(self, class_name): + """Check if the element has the specified class.""" return class_name in self def remove(self, *class_names): + """Remove one or more classes from the element's `classList`.""" for class_name in class_names: if isinstance(class_name, list): for item in class_name: @@ -283,10 +280,12 @@ def remove(self, *class_names): self._class_list.remove(class_name) def replace(self, old_class, new_class): + """""" self.remove(old_class) self.add(new_class) def toggle(self, *class_names): + """Toggle one or more classes in the element's `classList`.""" for class_name in class_names: if class_name in self: self.remove(class_name) @@ -303,6 +302,7 @@ class HasOptions: @property def options(self): + """Return the element's options as an `Options""" if not hasattr(self, "_options"): self._options = Options(self) @@ -310,14 +310,13 @@ def options(self): class Options: - """This class represents the