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

dfint / csv-bisect-gui / 15943650156

28 Jun 2025 11:25AM UTC coverage: 59.314% (+0.9%) from 58.435%
15943650156

push

github

insolor
Fix some ruff warnings, disable some other warnings

27 of 84 branches covered (32.14%)

Branch coverage included in aggregate %.

10 of 10 new or added lines in 3 files covered. (100.0%)

18 existing lines in 2 files now uncovered.

215 of 324 relevant lines covered (66.36%)

0.66 hits per line

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

67.53
/csv_bisect_gui/bisect_tool.py
1
from __future__ import annotations
1✔
2

3
import tkinter as tk
1✔
4
from functools import partial
1✔
5
from itertools import islice
1✔
6
from operator import itemgetter
1✔
7
from tkinter import ttk
1✔
8
from typing import TYPE_CHECKING, Any, Generic, TypeVar
1✔
9

10
from bidict import MutableBidict, bidict
1✔
11
from tkinter_layout_helpers import grid_manager, pack_manager
1✔
12

13
from csv_bisect_gui.scrollbar_frame import ScrollbarFrame
1✔
14

15
if TYPE_CHECKING:
16
    from collections.abc import Iterable, Iterator
17

18
T = TypeVar("T")
1✔
19

20

21
class Node(Generic[T]):
1✔
22
    _all_items: list[T]
1✔
23
    start: int
1✔
24
    end: int
1✔
25

26
    def __init__(self, items: list[T], start: int = 0, end: int | None = None) -> None:
1✔
27
        self._all_items = items
1✔
28
        self.start = start
1✔
29

30
        if end is None:
1✔
31
            end = len(items) - 1
1✔
32

33
        self.end = end
1✔
34
        assert self.start >= 0
1✔
35
        assert self.end < len(items)
1✔
36

37
    @property
1✔
38
    def size(self) -> int:
1✔
39
        return self.end - self.start + 1
1✔
40

41
    def split(self) -> tuple[Node[T], Node[T]]:
1✔
42
        assert self.size >= 2, f"Not enough items to split: {self.size}"
1✔
43
        mid = (self.start + self.end) // 2
1✔
44
        return Node(self._all_items, self.start, mid), Node(self._all_items, mid + 1, self.end)
1✔
45

46
    @property
1✔
47
    def tree_text(self) -> str:
1✔
48
        if self.size == 0:
1✔
49
            return "[] (0 strings)"
1✔
50
        if self.size == 1:
1✔
51
            return f"[{self.start} : {self.end}] (1 string)"
1✔
52
        return f"[{self.start} : {self.end}] ({self.size} strings)"
1✔
53

54
    @property
1✔
55
    def slice(self) -> slice:
1✔
56
        return slice(self.start, self.end + 1)
1✔
57

58
    @property
1✔
59
    def items(self) -> Iterable[T]:
1✔
60
        s = self.slice
1✔
61
        return islice(self._all_items, s.start, s.stop)
1✔
62

63
    @property
1✔
64
    def column_text(self) -> str:
1✔
65
        if self.start > self.end:
1✔
66
            return "<empty>"
1✔
67
        if self.start == self.end:
1✔
68
            item = self._all_items[self.start]
1✔
69
            return str(item)
1✔
70
        if self.end - self.start + 1 <= 2:  # One or two strings in the slice: show all strings
1✔
71
            return ",".join(map(str, self.items))
1✔
72
        # More strings: show the first and the last
73
        return f"{self._all_items[self.start]} ... {self._all_items[self.end]}"
1✔
74

75
    def __hash__(self) -> int:
1✔
UNCOV
76
        return hash((self.start, self.end))
×
77

78
    def __eq__(self, other: Node[T]) -> bool:
1✔
UNCOV
79
        return self.start == other.start and self.end == other.end
×
80

81
    def __repr__(self) -> str:
82
        return f"{self.__class__.__name__}(..., {self.start}, {self.end})"
83

84

85
class BisectTool(tk.Frame, Generic[T]):
1✔
86
    _strings: list[T] | None
1✔
87
    _nodes_by_item_ids: MutableBidict[str, Node[T]]
1✔
88

89
    def __init__(self, *args: list[Any], strings: list[T] | None = None, **kwargs: dict[str, Any]) -> None:
1✔
90
        super().__init__(*args, **kwargs)
1✔
91
        with grid_manager(self, sticky=tk.NSEW, pady=2) as grid:
1✔
92
            scrollbar_frame = ScrollbarFrame(widget_factory=ttk.Treeview, show_scrollbars=tk.VERTICAL)
1✔
93

94
            self.tree = tree = scrollbar_frame.widget
1✔
95
            tree["columns"] = ("strings",)
1✔
96
            tree.heading("#0", text="Tree")
1✔
97
            tree.heading("#1", text="Strings")
1✔
98

99
            self._nodes_by_item_ids = bidict()
1✔
100
            self.strings = strings
1✔
101

102
            grid.new_row().add(scrollbar_frame).configure(weight=1)
1✔
103

104
            with pack_manager(tk.Frame(), side=tk.LEFT, expand=True, fill=tk.X, padx=1) as toolbar:
1✔
105
                toolbar.pack_all(
1✔
106
                    ttk.Button(text="Split", command=self.split_selected_node),
107
                    ttk.Button(text="Mark as bad", command=partial(self.mark_selected_node, background="orange")),
108
                    ttk.Button(text="Mark as good", command=partial(self.mark_selected_node, background="lightgreen")),
109
                    ttk.Button(text="Clear mark", command=partial(self.mark_selected_node, background="white")),
110
                )
111

112
                grid.new_row().add(toolbar.parent)
1✔
113

114
            grid.columnconfigure(0, weight=1)
1✔
115

116
    @property
1✔
117
    def strings(self) -> list[T] | None:
1✔
UNCOV
118
        return self._strings
×
119

120
    @strings.setter
1✔
121
    def strings(self, value: list[T] | None) -> None:
1✔
122
        self._strings = value
1✔
123
        self.tree.delete(*self.tree.get_children())
1✔
124
        self._nodes_by_item_ids = bidict()  # Create new empty bidict to avoid ValueDuplicationError
1✔
125
        if value:
1!
UNCOV
126
            self.insert_node(Node[T](value))
×
127

128
    def insert_node(self, node: Node[T], parent_node: Node[T] | None = None) -> None:
1✔
129
        parent_item_id = "" if not parent_node else self._nodes_by_item_ids.inverse[parent_node]
×
130

131
        item_id = self.tree.insert(
×
132
            parent_item_id,
133
            tk.END,
134
            text=node.tree_text,
135
            values=(node.column_text,),
136
            open=True,
137
        )
138

UNCOV
139
        self._nodes_by_item_ids[item_id] = node
×
140

141
        # Add an item id as a tag to color the row by that tag
UNCOV
142
        self.tree.item(item_id, tags=(item_id,))
×
143

144
    def get_item_id_of_node(self, node: Node[T]) -> MutableBidict[Node[T], str]:
1✔
UNCOV
145
        return self._nodes_by_item_ids.inverse[node]
×
146

147
    def get_selected_node(self) -> Node[T] | None:
1✔
UNCOV
148
        tree = self.tree
×
UNCOV
149
        selected_ids = tree.selection()
×
150
        if selected_ids and not tree.get_children(selected_ids[0]):
×
151
            return self._nodes_by_item_ids[selected_ids[0]]
×
152
        return None
×
153

154
    def split_selected_node(self) -> None:
1✔
UNCOV
155
        parent = self.get_selected_node()
×
156
        if parent and parent.start != parent.end:
×
157
            new_nodes = parent.split()
×
158

UNCOV
159
            for node in new_nodes:
×
160
                self.insert_node(parent_node=parent, node=node)
×
161

162
            # move selection to the first child
UNCOV
163
            item_id = self._nodes_by_item_ids.inverse[new_nodes[0]]
×
164
            self.tree.selection_set(item_id)
×
165

166
    def mark_selected_node(self, **kwargs: dict[str, Any]) -> None:
1✔
UNCOV
167
        tree = self.tree
×
168
        for item in tree.selection():
×
169
            tree.tag_configure(item, **kwargs)
×
170

171
    @property
1✔
172
    def selected_nodes(self) -> Iterable[Node[T]]:
1✔
UNCOV
173
        return (self._nodes_by_item_ids[item_id] for item_id in self.tree.selection())
×
174

175
    @property
1✔
176
    def filtered_strings(self) -> Iterator[T]:
1✔
UNCOV
177
        nodes: list[Node[T]] = list(self.selected_nodes)
×
178
        if not nodes:
×
179
            return self._strings
×
180
        if len(nodes) == 1:
×
181
            # Only one row selected (optimized case)
182
            return islice(self._strings, nodes[0].start, nodes[0].end + 1)
×
183
        # Merge ranges when multiple rows selected
184
        enumerated_strings = list(enumerate(self._strings))
×
UNCOV
185
        strings = set()
×
UNCOV
186
        for node in nodes:
×
187
            strings |= set(islice(enumerated_strings, node.start, node.end + 1))
×
188

189
        # Restore original order of the strings
190
        return map(itemgetter(1), sorted(strings, key=itemgetter(0)))
×
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