123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- from __future__ import annotations
- from typing import Generic, Iterator, List, Optional, Type, TypeVar, Union, overload
- from typing_extensions import Self
- from .context import context
- from .element import Element
- from .elements.mixins.content_element import ContentElement
- from .elements.mixins.source_element import SourceElement
- from .elements.mixins.text_element import TextElement
- from .elements.notification import Notification
- from .elements.radio import Radio
- from .elements.select import Select
- from .elements.toggle import Toggle
- T = TypeVar('T', bound=Element)
- class ElementFilter(Generic[T]):
- DEFAULT_LOCAL_SCOPE = False
- @overload
- def __init__(self: ElementFilter[Element], *,
- marker: Union[str, List[str], None] = None,
- content: Union[str, List[str], None] = None,
- local_scope: bool = DEFAULT_LOCAL_SCOPE,
- ) -> None:
- ...
- @overload
- def __init__(self, *,
- kind: Type[T],
- marker: Union[str, List[str], None] = None,
- content: Union[str, List[str], None] = None,
- local_scope: bool = DEFAULT_LOCAL_SCOPE,
- ) -> None:
- ...
- def __init__(self, *,
- kind: Optional[Type[T]] = None,
- marker: Union[str, List[str], None] = None,
- content: Union[str, List[str], None] = None,
- local_scope: bool = DEFAULT_LOCAL_SCOPE,
- ) -> None:
- """ElementFilter
- Sometimes it is handy to search the Python element tree of the current page.
- ``ElementFilter()`` allows powerful filtering by kind of elements, markers and content.
- It also provides a fluent interface to apply more filters like excluding elements or filtering for elements within a specific parent.
- The filter can be used as an iterator to iterate over the found elements and is always applied while iterating and not when being instantiated.
- And element is yielded if it matches all of the following conditions:
- - The element is of the specified kind (if specified).
- - The element is none of the excluded kinds.
- - The element has all of the specified markers.
- - The element has none of the excluded markers.
- - The element contains all of the specified content.
- - The element contains none of the excluded content.
- - Its ancestors include all of the specified instances defined via ``within``.
- - Its ancestors include none of the specified instances defined via ``not_within``.
- - Its ancestors include all of the specified kinds defined via ``within``.
- - Its ancestors include none of the specified kinds defined via ``not_within``.
- - Its ancestors include all of the specified markers defined via ``within``.
- - Its ancestors include none of the specified markers defined via ``not_within``.
- Element "content" includes its text, label, icon, placeholder, value, message, content, source.
- Partial matches like "Hello" in "Hello World!" are sufficient for content filtering.
- :param kind: filter by element type; the iterator will be of type ``kind``
- :param marker: filter by element markers; can be a list of strings or a single string where markers are separated by whitespace
- :param content: filter for elements which contain ``content`` in one of their content attributes like ``.text``, ``.value``, ``.source``, ...; can be a singe string or a list of strings which all must match
- :param local_scope: if `True`, only elements within the current scope are returned; by default the whole page is searched (this default behavior can be changed with ``ElementFilter.DEFAULT_LOCAL_SCOPE = True``)
- """
- self._kind = kind
- self._markers = marker.split() if isinstance(marker, str) else marker or []
- self._contents = [content] if isinstance(content, str) else content or []
- self._within_kinds: List[Type[Element]] = []
- self._within_instances: List[Element] = []
- self._within_markers: List[str] = []
- self._not_within_kinds: List[Type[Element]] = []
- self._not_within_instances: List[Element] = []
- self._not_within_markers: List[str] = []
- self._exclude_kinds: List[Type[Element]] = []
- self._exclude_markers: List[str] = []
- self._exclude_content: List[str] = []
- self._scope = context.slot.parent if local_scope else context.client.layout
- def __iter__(self) -> Iterator[T]:
- for element in self._scope.descendants():
- if self._kind and not isinstance(element, self._kind):
- continue
- if self._exclude_kinds and isinstance(element, tuple(self._exclude_kinds)):
- continue
- if any(marker not in element._markers for marker in self._markers):
- continue
- if any(marker in element._markers for marker in self._exclude_markers):
- continue
- if self._contents or self._exclude_content:
- element_contents = [content for content in (
- element.props.get('text'),
- element.props.get('label'),
- element.props.get('icon'),
- element.props.get('placeholder'),
- element.props.get('value'),
- element.props.get('error-message'),
- element.text if isinstance(element, TextElement) else None,
- element.content if isinstance(element, ContentElement) else None,
- element.source if isinstance(element, SourceElement) else None,
- ) if content]
- if isinstance(element, Notification):
- element_contents.append(element.message)
- if isinstance(element, (Select, Radio, Toggle)):
- options = {option['value']: option['label'] for option in element.props.get('options', [])}
- element_contents.append(options.get(element.value, ''))
- if not isinstance(element, Select) or element.is_showing_popup:
- element_contents.extend(options.values())
- if any(all(needle not in str(haystack) for haystack in element_contents) for needle in self._contents):
- continue
- if any(needle in str(haystack) for haystack in element_contents for needle in self._exclude_content):
- continue
- ancestors = set(element.ancestors())
- if self._within_instances and not ancestors.issuperset(self._within_instances):
- continue
- if self._not_within_instances and not ancestors.isdisjoint(self._not_within_instances):
- continue
- if self._within_kinds and not all(any(isinstance(ancestor, kind) for ancestor in ancestors) for kind in self._within_kinds):
- continue
- if self._not_within_kinds and any(isinstance(ancestor, tuple(self._not_within_kinds)) for ancestor in ancestors):
- continue
- ancestor_markers = {marker for ancestor in ancestors for marker in ancestor._markers}
- if self._within_markers and not ancestor_markers.issuperset(self._within_markers):
- continue
- if self._not_within_markers and not ancestor_markers.isdisjoint(self._not_within_markers):
- continue
- yield element # type: ignore
- def within(self, *,
- kind: Optional[Type[Element]] = None,
- marker: Optional[str] = None,
- instance: Union[Element, List[Element], None] = None,
- ) -> Self:
- """Filter elements which have a specific match in the parent hierarchy."""
- if kind is not None:
- assert issubclass(kind, Element)
- self._within_kinds.append(kind)
- if marker is not None:
- self._within_markers.extend(marker.split())
- if instance is not None:
- self._within_instances.extend(instance if isinstance(instance, list) else [instance])
- return self
- def exclude(self, *,
- kind: Optional[Type[Element]] = None,
- marker: Optional[str] = None,
- content: Optional[str] = None,
- ) -> Self:
- """Exclude elements with specific element type, marker or content."""
- if kind is not None:
- assert issubclass(kind, Element)
- self._exclude_kinds.append(kind)
- if marker is not None:
- self._exclude_markers.append(marker)
- if content is not None:
- self._exclude_content.append(content)
- return self
- def not_within(self, *,
- kind: Optional[Type[Element]] = None,
- marker: Optional[str] = None,
- instance: Union[Element, List[Element], None] = None,
- ) -> Self:
- """Exclude elements which have a parent of a specific type or marker."""
- if kind is not None:
- assert issubclass(kind, Element)
- self._not_within_kinds.append(kind)
- if marker is not None:
- self._not_within_markers.extend(marker.split())
- if instance is not None:
- self._not_within_instances.extend(instance if isinstance(instance, list) else [instance])
- return self
- def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) -> Self:
- """Apply, remove, or replace HTML classes.
- This allows modifying the look of the element or its layout using `Tailwind <https://tailwindcss.com/>`_ or `Quasar <https://quasar.dev/>`_ classes.
- Removing or replacing classes can be helpful if predefined classes are not desired.
- :param add: whitespace-delimited string of classes
- :param remove: whitespace-delimited string of classes to remove from the element
- :param replace: whitespace-delimited string of classes to use instead of existing ones
- """
- for element in self:
- element.classes(add, remove=remove, replace=replace)
- return self
- def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) -> Self:
- """Apply, remove, or replace CSS definitions.
- Removing or replacing styles can be helpful if the predefined style is not desired.
- :param add: semicolon-separated list of styles to add to the element
- :param remove: semicolon-separated list of styles to remove from the element
- :param replace: semicolon-separated list of styles to use instead of existing ones
- """
- for element in self:
- element.style(add, remove=remove, replace=replace)
- return self
- def props(self, add: Optional[str] = None, *, remove: Optional[str] = None) -> Self:
- """Add or remove props.
- This allows modifying the look of the element or its layout using `Quasar <https://quasar.dev/>`_ props.
- Since props are simply applied as HTML attributes, they can be used with any HTML element.
- Boolean properties are assumed ``True`` if no value is specified.
- :param add: whitespace-delimited list of either boolean values or key=value pair to add
- :param remove: whitespace-delimited list of property keys to remove
- """
- for element in self:
- element.props(add, remove=remove)
- return self
|