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

Hekxsler / pudding / 25738877031

12 May 2026 01:50PM UTC coverage: 92.24% (-1.0%) from 93.254%
25738877031

Pull #5

github

web-flow
[token]: fix linting
Pull Request #5: Feature/specifiy output

1260 of 1366 relevant lines covered (92.24%)

0.92 hits per line

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

92.55
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

8
class Node:
1✔
9
    """Class representing a node."""
10

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

171
    def get_children(self) -> list[Self]:
1✔
172
        """Return a list of Nodes that are children to this node."""
173
        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