Source code for pudding.writer.writers.xml

"""Module defining xml writer class."""

from pathlib import Path

from lxml import etree

from ..node import Node
from .writer import BufferedWriter, Writer


class SliXml(Writer):
    """Writer class for slim xml output."""

    def __init__(
        self, file_path: Path, *, encoding: str = "utf-8", root_name: str = "xml"
    ) -> None:
        """Init for SliXml class."""
        super().__init__(file_path, encoding=encoding, root_name=root_name)
        self.last_single = False
        self.last_closing = False
        self.last_indent = 0
        self.indent = 1
        self.file = open(file_path, "w", encoding=encoding)
        self.last_node: Node = Node(root_name)
        self.prev_roots: list[str] = [root_name]

    def _writenode(
        self, node: Node, single: bool = False, closing: bool = False
    ) -> None:
        """Write node to file."""
        self._writeline(
            self._to_tag(
                self.last_node.name,
                self.last_node.attribs,
                self.last_node.text,
                single=self.last_single,
                closing=self.last_closing,
            )
        )
        self.last_indent = self.indent
        self.last_node = node
        self.last_single = single
        self.last_closing = closing

    def _writeline(self, line: str) -> None:
        """Write line with indent to output."""
        self.file.write(f"{'  '*self.last_indent}{line}\n")

    def _to_tag(
        self,
        name: str,
        attributes: dict[str, str],
        value: str | None = None,
        single: bool = False,
        closing: bool = False,
    ) -> str:
        """Create an xml tag.

        :param name: Name of the tag.
        :param attributes: Attributes of the tag.
        :param value: Text of this tag.
        :param open: If no value is set, this determines if a tag is closed or not.
        :returns str: XML-Tag as a string.
        """
        tag = name.casefold()
        attribs = [f' {k}="{v}"' for k, v in attributes.items()]
        xml = f"{tag}{''.join(attribs)}"
        if value is not None:
            return f"<{xml}>{value}</{tag}>"
        return f"<{'/'*(closing)}{xml}{'/'*(single and not closing)}>"

    def create_element(self, path: str, value: str | None = None) -> None:
        """Add an element to the current node.

        :param path: Path of the element.
        :param value: Value of the element or None if it has no value.
        """
        paths = Node.split_path(path)
        match len(paths):
            case 0:
                raise ValueError(f"Invalid path {repr(path)}.")
            case 1:
                node = Node.from_path(paths[0][0], value)
                self._writenode(node, single=True)
            case _:
                for sub_paths in paths[:-1]:
                    self._writenode(Node.from_path(sub_paths[0], value))
                self._writenode(Node.from_path(paths[-1][0], value), single=True)
                for sub_paths in reversed(paths[:-1]):
                    self._writenode(Node.from_path(sub_paths[2], value), closing=True)

    def add_element(self, path: str, value: str | None = None) -> None:
        """Add an element if its not the current element.

        Otherwise it appends the string to the already existing element.

        :param path: Path to the element.
        :param value: Value of the element or None if it has no value.
        """
        if self.last_node == Node.from_path(path):
            if not value:
                return
            if not self.last_node.text:
                self.last_node.text = value
            else:
                self.last_node.text += value
        else:
            self.create_element(path, value)

    def enter_path(self, path: str, value: str | None = None) -> None:
        """Enter a node and create elements in the path.

        :param path: Path to the element.
        :param value: Value of the element or None if it has no value.
        """
        paths = Node.split_path(path)
        for node_path, _, _, _ in paths:
            self._writenode(Node.from_path(node_path))
            self.indent += 1
        self.last_node.text = value
        self.prev_roots.append("/".join([p[2] for p in paths]))

    def open_path(self, path: str, value: str | None = None) -> None:
        """Enter a node and create elements in the path.

        Always creates the last node.

        :param path: Path to the element.
        :param value: Value of the element or None if it has no value.
        """
        return self.enter_path(path, value)

    def leave_paths(self, amount: int = 1) -> None:
        """Leave the previously entered path."""
        for _ in range(amount):
            last_root = self.prev_roots.pop()
            for node in reversed(last_root.split("/")):
                self.indent -= 1
                self._writenode(Node(node), closing=True)

    def write_output(self) -> None:
        """Write last open nodes to file."""
        self.leave_paths(len(self.prev_roots))
        # write last node again because it buffers the last node
        self._writenode(self.last_node, closing=True)


[docs] class Xml(BufferedWriter): """Writer class for xml output.""" def __init__( self, file_path: Path, *, encoding: str = "utf-8", root_name: str = "xml" ) -> None: """Init XML writer.""" super().__init__(file_path, encoding=encoding, root_name=root_name)
[docs] def serialize_node(self, node: Node) -> etree.Element: """Convert node object to etree element.""" root = etree.Element(node.name, node.attribs) root.text = node.text for child in node.get_sorted_children(): root.append(self.serialize_node(child)) return root
[docs] def generate_output(self) -> str: """Generate output in specified format.""" self.root.name = self.root_name tree = self.serialize_node(self.root) return etree.tostring(tree, pretty_print=True, encoding=str)
[docs] def write_output(self) -> None: """Write generated output to file.""" self.root.name = self.root_name etree.ElementTree(self.serialize_node(self.root)).write( self.file_path, encoding=self.encoding, pretty_print=True, xml_declaration=False, )