• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Hekxsler / pudding / 26163860904

20 May 2026 12:51PM UTC coverage: 92.385% (-0.9%) from 93.254%
26163860904

Pull #5

github

web-flow
[datatypes]: Adjust error message for invalid type
Pull Request #5: Feature/specifiy output

1286 of 1392 relevant lines covered (92.39%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.63
pudding/writer/node.py
1
"""Node class for caching generated output."""
2

3
import re
1✔
4
from itertools import chain
1✔
5
from typing import Self
1✔
6

7
from pudding.exceptions import ValueError
1✔
8

9

10
class Node:
1✔
11
    """Class representing a node."""
12

13
    attribute_re = re.compile(r"([?&]([\w\-\_]+)=\"((?:\\\"|[^\"])+)\")")
1✔
14
    node_re = re.compile(
1✔
15
        r"((\.)()()|(/?)([\w\-\_ ]+)((?:[?&][\w\-\_]+=\"(?:\\\"|[^\"])+\")*))"
16
    )
17

18
    def __init__(
1✔
19
        self,
20
        name: str,
21
        attributes: dict[str, str] | None = None,
22
        text: str | None = None,
23
    ) -> None:
24
        """Init for Node class.
25

26
        :param name: Name of this node.
27
        :param attributes: Attributes of this node.
28
        :param text: Text value of this node.
29
        """
30
        if attributes is None:
1✔
31
            attributes = {}
1✔
32
        self.attribs = attributes
1✔
33
        self.name = name
1✔
34
        self.children: dict[str, list[Self]] = {}
1✔
35
        self.text = text
1✔
36
        self.parent: Self | None = None
1✔
37

38
    def __eq__(self, other: object) -> bool:
1✔
39
        """Compare Node object to other object.
40

41
        To equal only name and attributes have to be the same.
42
        Text and children of the nodes are ignored.
43
        """
44
        if not isinstance(other, self.__class__):
1✔
45
            return False
×
46
        if self.name == other.name and self.attribs == other.attribs:
1✔
47
            return True
×
48
        return False
1✔
49

50
    def __repr__(self) -> str:
51
        """Represent node as string."""
52
        return f"<Node name={repr(self.name)} {self.attribs} children={self.children}>"
53

54
    @classmethod
1✔
55
    def from_node_path(cls, path: str, text: str | None = None) -> Self:
1✔
56
        """Parse node object from path.
57

58
        :param path: Node path of the object.
59
        :param text: Text of the created node object.
60
        :returns Node: The created node object.
61
        """
62
        return cls(*cls.parse_node_path(path), text)
1✔
63

64
    @classmethod
1✔
65
    def parse_node_path(cls, path: str) -> tuple[str, dict[str, str]]:
1✔
66
        """Read tag name and attributes from an node.
67

68
        :param path: Path node to parse.
69
        :returns: Tuple with name as string and attributes as a dict.
70
        """
71
        attributes: dict[str, str] = {}
1✔
72
        path = path.lstrip("./")
1✔
73
        for attribute in cls.attribute_re.findall(path):
1✔
74
            attributes[attribute[1]] = attribute[2]
1✔
75
            path = path.replace(attribute[0], "")
1✔
76
        if "/" in path:
1✔
77
            raise ValueError(f"Path {path} contains more than one node.")
×
78
        path = path.replace(" ", "-")
1✔
79
        return path.casefold(), attributes
1✔
80

81
    @classmethod
1✔
82
    def split_path(cls, path: str) -> list[tuple[str, str, str, str]]:
1✔
83
        """Parse and split a path into nodes.
84

85
        :param path: Path to split.
86
        :returns: List of node matches as a tuple.
87
            E.g. [(full_nodepath, [./]*, tag, attributes), ...]
88
        """
89
        if "/" not in path:
1✔
90
            if path == ".":
1✔
91
                return [(".", "", "", "")]
1✔
92
            match = cls.node_re.fullmatch(path)
1✔
93
            if match:
1✔
94
                groups = match.groups("")
1✔
95
                return [(groups[0], groups[4], groups[5], groups[6])]
1✔
96
        else:
97
            matches: list[tuple[str, str, str, str]] = []
1✔
98
            match = cls.node_re.match(path)
1✔
99
            while match:
1✔
100
                if match[0] == ".":
1✔
101
                    matches.append((".", "", "", ""))
×
102
                    continue
×
103
                groups = match.groups("")
1✔
104
                matches.append((groups[0], groups[4], groups[5], groups[6]))
1✔
105
                path = path[len(match.group(0)) :]
1✔
106
                match = cls.node_re.match(path)
1✔
107
            if matches and not path:
1✔
108
                # remaining chars in path string -> no fullmatch -> invalid path
109
                return matches
1✔
110
        raise ValueError(f"Invalid path {repr(path)}.")
×
111

112
    @property
1✔
113
    def node_path(self) -> str:
1✔
114
        """Return node as path."""
115
        attributes = "?"
1✔
116
        for k, v in self.attribs.items():
1✔
117
            attributes += f'{k}="{v}"&'
1✔
118
        return f"{self.name}{attributes[:-1]}"
1✔
119

120
    def add_child(self, node_path: str, text: str | None = None) -> Self:
1✔
121
        """Create a child node of this node.
122

123
        :param node_path: Path of the node to add.
124
        :returns Node: The created and added node.
125
        """
126
        node_path = node_path.lstrip("./")
1✔
127
        node = self.from_node_path(node_path, text)
1✔
128
        node.parent = self
1✔
129
        childs = self.children.get(node_path, [])
1✔
130
        self.children[node_path] = childs + [node]
1✔
131
        return node
1✔
132

133
    def find(self, path: str) -> Self | None:
1✔
134
        """Find a child in the given path.
135

136
        :param path: Path to the node.
137
        :returns: The node or None if it does not exist.
138
        """
139
        if path == ".":
1✔
140
            return self
1✔
141
        if not self.children:
1✔
142
            return None
1✔
143
        root = self
1✔
144
        for node_path in (path[0] for path in self.split_path(path)):
1✔
145
            childs = root.children.get(node_path.lstrip("./"), [])
1✔
146
            if childs:
1✔
147
                root = childs[0]
1✔
148
                continue
1✔
149
            return None
1✔
150
        return root
1✔
151

152
    def set(self: Self, name: str, value: str) -> None:
1✔
153
        """Set an attribute of this node.
154

155
        :param name: Name of the attribute.
156
        :param value: Value of the attribute.
157
        """
158
        if self.parent:
1✔
159
            child_list = self.parent.children.get(self.node_path, [])
1✔
160
            child_list.remove(self)
1✔
161
        self.attribs[name] = value
1✔
162
        if self.parent:
1✔
163
            child_list = self.parent.children.get(self.node_path, [])
1✔
164
            self.parent.children[self.node_path] = child_list + [self]
1✔
165

166
    def get(self, name: str, default: None = None) -> str | None:
1✔
167
        """Get an attribute of this node.
168

169
        :param name: Name of the attribute.
170
        """
171
        return self.attribs.get(name, default)
×
172

173
    def get_children(self) -> list[Self]:
1✔
174
        """Return a list of Nodes that are children to this node."""
175
        return list(chain(*self.children.values()))
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc