Skip to content

htmy.etree

ETreeConverter

Utility for converting XML strings to custom components.

By default the converter uses the standard library's xml.etree.ElementTree module for string to element tree, and element tree to string conversion, but if lxml is installed, it will be used instead.

Installing lxml is recommended for better performance and additional features, like performance and support for broken HTML fragments. Important: lxml is far more lenient and flexible than the standard library, so having it installed is not only a performance boost, but it may also slightly change the element conversion behavior in certain edge-cases!

Source code in htmy/etree.py
class ETreeConverter:
    """
    Utility for converting XML strings to custom components.

    By default the converter uses the standard library's `xml.etree.ElementTree`
    module for string to element tree, and element tree to string conversion,
    but if `lxml` is installed, it will be used instead.

    Installing `lxml` is recommended for better performance and additional features,
    like performance and support for broken HTML fragments. **Important:** `lxml` is
    far more lenient and flexible than the standard library, so having it installed is
    not only a performance boost, but it may also slightly change the element conversion
    behavior in certain edge-cases!
    """

    __slots__ = ("_rules",)

    _htmy_fragment: ClassVar[str] = "htmy_fragment"
    """
    Placeholder tag name that's used to wrap possibly multi-root XML snippets into a valid
    XML document with a single root that can be processed by standard tools.
    """

    def __init__(self, rules: Mapping[str, Callable[..., ComponentType]]) -> None:
        """
        Initialization.

        Arguments:
            rules: Tag-name to component conversion rules.
        """
        self._rules = rules

    def convert(self, element: str) -> ComponentType:
        """Converts the given (possibly multi-root) XML string to a component."""
        if len(self._rules) == 0:
            return SafeStr(element)

        element = f"<{self._htmy_fragment}>{element}</{self._htmy_fragment}>"
        return self.convert_element(etree_from_string(element))  # noqa: S314 # Only use XML strings from a trusted source.

    def convert_element(self, element: Element) -> ComponentType:
        """Converts the given `Element` to a component."""
        rules = self._rules
        if len(rules) == 0:
            return SafeStr(etree_to_string(element, encoding="unicode"))

        tag: str = element.tag  # type: ignore[assignment]
        component = Fragment if tag == self._htmy_fragment else rules.get(tag)
        children = self._convert_children(element)
        properties = self._convert_properties(element)

        return (
            WildcardTag(*children, htmy_name=tag, **properties)
            if component is None
            else component(
                *children,
                **properties,
            )
        )

    def _convert_properties(self, element: Element) -> Properties:
        """
        Converts the attributes of the given `Element` to a `Properties` mapping.

        This method should not alter property names in any way.
        """
        return {key: unescape(value) for key, value in element.items()}

    def _convert_children(self, element: Element) -> Generator[ComponentType, None, None]:
        """
        Generator that converts all (text and `Element`) children of the given `Element` to a component.
        """
        if text := self._process_text(element.text):
            yield text

        for child in element:
            yield self.convert_element(child)
            if tail := self._process_text(child.tail):
                yield tail

    def _process_text(self, escaped_text: str | None) -> str | None:
        """Processes a single XML-escaped text child."""
        return unescape(escaped_text) if escaped_text else None

__init__(rules)

Initialization.

Parameters:

Name Type Description Default
rules Mapping[str, Callable[..., ComponentType]]

Tag-name to component conversion rules.

required
Source code in htmy/etree.py
def __init__(self, rules: Mapping[str, Callable[..., ComponentType]]) -> None:
    """
    Initialization.

    Arguments:
        rules: Tag-name to component conversion rules.
    """
    self._rules = rules

convert(element)

Converts the given (possibly multi-root) XML string to a component.

Source code in htmy/etree.py
def convert(self, element: str) -> ComponentType:
    """Converts the given (possibly multi-root) XML string to a component."""
    if len(self._rules) == 0:
        return SafeStr(element)

    element = f"<{self._htmy_fragment}>{element}</{self._htmy_fragment}>"
    return self.convert_element(etree_from_string(element))  # noqa: S314 # Only use XML strings from a trusted source.

convert_element(element)

Converts the given Element to a component.

Source code in htmy/etree.py
def convert_element(self, element: Element) -> ComponentType:
    """Converts the given `Element` to a component."""
    rules = self._rules
    if len(rules) == 0:
        return SafeStr(etree_to_string(element, encoding="unicode"))

    tag: str = element.tag  # type: ignore[assignment]
    component = Fragment if tag == self._htmy_fragment else rules.get(tag)
    children = self._convert_children(element)
    properties = self._convert_properties(element)

    return (
        WildcardTag(*children, htmy_name=tag, **properties)
        if component is None
        else component(
            *children,
            **properties,
        )
    )