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

Hekxsler / pudding / 25528314821

07 May 2026 11:46PM UTC coverage: 88.962% (-0.3%) from 89.239%
25528314821

Pull #3

github

web-flow
[toml]: ignore __main__ and version.py in coveralls
Pull Request #3: Feature: control statements

1225 of 1377 relevant lines covered (88.96%)

0.89 hits per line

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

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

3
from itertools import chain
1✔
4
import re
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_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
        """Split the 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
            match = cls.node_re.fullmatch(path)
1✔
89
            if match:
1✔
90
                groups = match.groups("")[:4]
1✔
91
                if len(groups) == 4:  # needed for typing
1✔
92
                    return [groups]
1✔
93
        else:
94
            matches: list[tuple[str, str, str, str]] = []
1✔
95
            match = cls.node_re.match(path)
1✔
96
            while match:
1✔
97
                groups = match.groups("")[:4]
1✔
98
                if len(groups) == 4:  # needed for typing
1✔
99
                    matches.append(groups)
1✔
100
                path = path[len(match.group(0)):]
1✔
101
                match = cls.node_re.match(path)
1✔
102
            if matches:
1✔
103
                return matches
1✔
104
        raise ValueError(f"Invalid path {repr(path)}.")
×
105

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

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

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

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

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

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

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

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

163
        :param name: Name of the attribute.
164
        """
165
        return self.attribs.get(name, default)
×
166

167
    def get_sorted_children(self) -> list[Self]:
1✔
168
        """Return a list of children sorted by name."""
169
        childs = chain(*self.children.values())
1✔
170
        return sorted(childs, key=lambda x: x.name)
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