Source code for pudding.writer.node

"""Node class for caching generated output."""

from itertools import chain
import re
from typing import Self


[docs] class Node: """Class representing a node.""" attribute_re = re.compile(r"([?&]([\w\-\_]+)=\"((?:\\\"|[^\"])+)\")") node_re = re.compile( r"(([./]?)([\w\-\_ ]+)((?:[?&][\w\-\_]+=\"(?:\\\"|[^\"])+\")*))" ) def __init__( self, name: str, attributes: dict[str, str] | None = None, text: str | None = None, ) -> None: """Init for Node class. :param name: Name of this node. :param attributes: Attributes of this node. :param text: Text value of this node. """ if attributes is None: attributes = {} self.attribs = attributes self.name = name self.children: dict[str, list[Self]] = {} self.text = text self.parent: Self | None = None def __eq__(self, other: object) -> bool: """Compare Node object to other object. To equal only name and attributes have to be the same. Text and children of the nodes are ignored. """ if not isinstance(other, self.__class__): return False if self.name == other.name and self.attribs == other.attribs: return True return False def __repr__(self) -> str: """Represent node as string.""" return f"<Node name={repr(self.name)} {self.attribs} children={self.children}>"
[docs] @classmethod def from_path(cls, path: str, text: str | None = None) -> Self: """Parse node object from path. :param path: Node path of the object. :param text: Text of the created node object. :returns Node: The created node object. """ return cls(*cls.parse_node_path(path), text)
[docs] @classmethod def parse_node_path(cls, path: str) -> tuple[str, dict[str, str]]: """Read tag name and attributes from an node. :param path: Path node to parse. :returns: Tuple with name as string and attributes as a dict. """ attributes: dict[str, str] = {} path = path.lstrip("./") for attribute in cls.attribute_re.findall(path): attributes[attribute[1]] = attribute[2] path = path.replace(attribute[0], "") if "/" in path: raise ValueError(f"Path {path} contains more than one node.") path = path.replace(" ", "-") return path.casefold(), attributes
[docs] @classmethod def split_path(cls, path: str) -> list[tuple[str, str, str, str]]: """Split the path into nodes. :param path: Path to split. :returns: List of node matches as a tuple. E.g. [(full_nodepath, [./]*, tag, attributes), ...] """ if "/" not in path: match = cls.node_re.fullmatch(path) if match: groups = match.groups("")[:4] if len(groups) == 4: # needed for typing return [groups] else: matches: list[tuple[str, str, str, str]] = [] match = cls.node_re.match(path) while match: groups = match.groups("")[:4] if len(groups) == 4: # needed for typing matches.append(groups) path = path[len(match.group(0)):] match = cls.node_re.match(path) if matches: return matches raise ValueError(f"Invalid path {repr(path)}.")
@property def node_path(self) -> str: """Return node as path.""" attributes = "?" for k, v in self.attribs.items(): attributes += f'{k}="{v}"&' return f"{self.name}{attributes[:-1]}"
[docs] def add_child(self, node_path: str, text: str | None = None) -> Self: """Create a child node of this node. :param node_path: Path of the node to add. :returns Node: The created and added node. """ node_path = node_path.lstrip("./") node = self.from_path(node_path, text) node.parent = self childs = self.children.get(node_path, []) self.children[node_path] = childs + [node] return node
[docs] def find(self, path: str) -> Self | None: """Find a child in the given path. :param path: Path to the node. :returns: The node or None if it does not exist. """ if path == ".": return self if not self.children: return None root = self for node_path in (path[0] for path in self.split_path(path)): childs = root.children.get(node_path.lstrip("./"), []) if childs: root = childs[0] continue return None return root
[docs] def set(self: Self, name: str, value: str) -> None: """Set an attribute of this node. :param name: Name of the attribute. :param value: Value of the attribute. """ if self.parent: child_list = self.parent.children.get(self.node_path, []) child_list.remove(self) self.attribs[name] = value if self.parent: child_list = self.parent.children.get(self.node_path, []) self.parent.children[self.node_path] = child_list + [self]
[docs] def get(self, name: str, default: None = None) -> str | None: """Get an attribute of this node. :param name: Name of the attribute. """ return self.attribs.get(name, default)
[docs] def get_sorted_children(self) -> list[Self]: """Return a list of children sorted by name.""" childs = chain(*self.children.values()) return sorted(childs, key=lambda x: x.name)