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

zelazna / nw_bot / 25252883500

02 May 2026 01:22PM UTC coverage: 81.605% (-17.5%) from 99.068%
25252883500

Pull #17

github

zelazna
[FIX] Fix Python 3.12 coverage tracer crash in QThread

Call sys.settrace(None) at the start of Worker.run() to detach the
CPython coverage C tracer before the thread executes. This prevents
the Python 3.12 crash that occurred when a QThread exited with an
active coverage tracer, which was the root cause of flaky E2E tests
on Linux CI. Revert CI to a single xvfb-run pytest invocation now
that the split workaround is no longer needed.
Pull Request #17: Refacto command pattern

179 of 211 new or added lines in 14 files covered. (84.83%)

156 existing lines in 13 files now uncovered.

834 of 1022 relevant lines covered (81.6%)

0.82 hits per line

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

42.17
/bot/models/command_list.py
1
import logging
1✔
2
import pickle
1✔
3
from collections.abc import Sequence
1✔
4
from typing import final, override
1✔
5

6
from PySide6.QtCore import (
1✔
7
    QAbstractListModel,
8
    QByteArray,
9
    QMimeData,
10
    QModelIndex,
11
    QObject,
12
    QPersistentModelIndex,
13
    Qt,
14
)
15

16
from bot.core.constants import MIME_TYPE
1✔
17
from bot.models.keyboard import DirectionalKeystroke, Keystroke
1✔
18
from bot.models.mouse import MouseClick
1✔
19

20
Index = QModelIndex | QPersistentModelIndex
1✔
21

22

23
@final
1✔
24
class CommandListModel(QAbstractListModel):
1✔
25
    commands: list[Keystroke | DirectionalKeystroke | MouseClick]
1✔
26

27
    def __init__(
1✔
28
        self,
29
        commands: list[Keystroke | DirectionalKeystroke | MouseClick] | None = None,
30
        parent: QObject | None = None,
31
    ):
32
        super().__init__(parent)
1✔
33
        self.commands = commands if commands else []
1✔
34

35
    @override
1✔
36
    def data(self, index: Index, role: int = 0) -> object:
1✔
NEW
37
        if not index.isValid():
×
NEW
38
            return None
×
NEW
39
        if not (0 <= index.row() < len(self.commands)):
×
NEW
40
            return None
×
NEW
41
        command = self.commands[index.row()]
×
UNCOV
42
        if role == Qt.ItemDataRole.DisplayRole:
×
UNCOV
43
            return repr(command)
×
NEW
44
        if role == Qt.ItemDataRole.UserRole:
×
NEW
45
            return command
×
NEW
46
        return None
×
47

48
    @override
1✔
49
    def rowCount(self, parent: Index = QModelIndex()) -> int:  # pyright: ignore[reportCallInDefaultInitializer]
1✔
50
        return len(self.commands)
1✔
51

52
    @override
1✔
53
    def flags(self, index: Index) -> Qt.ItemFlag:
1✔
UNCOV
54
        flags = super().flags(index)
×
UNCOV
55
        if index.isValid():
×
NEW
56
            flags |= Qt.ItemFlag.ItemIsDragEnabled
×
57
        else:
NEW
58
            flags |= Qt.ItemFlag.ItemIsDropEnabled
×
UNCOV
59
        return flags
×
60

61
    @override
1✔
62
    def supportedDropActions(self) -> Qt.DropAction:
1✔
NEW
63
        return Qt.DropAction.MoveAction
×
64

65
    @override
1✔
66
    def mimeTypes(self) -> list[str]:
1✔
UNCOV
67
        types = super().mimeTypes()
×
UNCOV
68
        types.append(MIME_TYPE)
×
UNCOV
69
        return types
×
70

71
    @override
1✔
72
    def mimeData(self, indexes: Sequence[QModelIndex]) -> QMimeData:
1✔
UNCOV
73
        mimeData = QMimeData()
×
UNCOV
74
        data = QByteArray()
×
75

UNCOV
76
        for idx in indexes:
×
UNCOV
77
            if idx.isValid():
×
NEW
78
                row = idx.row()
×
NEW
79
                data.append(pickle.dumps((row, self.commands[row])))
×
80

UNCOV
81
        mimeData.setData(MIME_TYPE, data)
×
UNCOV
82
        return mimeData
×
83

84
    def add_command(
1✔
85
        self, command: Keystroke | DirectionalKeystroke | MouseClick
86
    ) -> None:
87
        self.commands.append(command)
1✔
88
        self.layoutChanged.emit()
1✔
89

90
    @override
1✔
91
    def canDropMimeData(
1✔
92
        self,
93
        data: QMimeData,
94
        action: Qt.DropAction,
95
        row: int,
96
        column: int,
97
        parent: Index,
98
    ) -> bool:
UNCOV
99
        if not data.hasFormat(MIME_TYPE):
×
UNCOV
100
            return False
×
UNCOV
101
        if column > 0:
×
UNCOV
102
            return False
×
UNCOV
103
        return True
×
104

105
    @override
1✔
106
    def dropMimeData(
1✔
107
        self,
108
        data: QMimeData,
109
        action: Qt.DropAction,
110
        row: int,
111
        column: int,
112
        parent: Index,
113
    ) -> bool:
UNCOV
114
        if not self.canDropMimeData(data, action, row, column, parent):
×
UNCOV
115
            return False
×
UNCOV
116
        if action is Qt.DropAction.IgnoreAction:
×
UNCOV
117
            return True
×
NEW
118
        insert_at = row if row != -1 else self.rowCount(QModelIndex())
×
NEW
119
        before_index, command = pickle.loads(data.data(MIME_TYPE).data())  # pyright: ignore[reportAny]
×
120

NEW
121
        logging.debug(f"Item {command} from idx {before_index} to idx {insert_at}")
×
122

NEW
123
        if before_index == insert_at or before_index + 1 == insert_at:
×
NEW
124
            return True
×
125

NEW
126
        root = QModelIndex()
×
127
        if not self.beginMoveRows(root, before_index, before_index, root, insert_at):  # pyright: ignore[reportAny]  # pragma: no cover
128
            return True  # pragma: no cover
NEW
129
        item = self.commands.pop(before_index)  # pyright: ignore[reportAny]
×
NEW
130
        if before_index < insert_at:
×
NEW
131
            insert_at -= 1
×
NEW
132
        self.commands.insert(insert_at, item)
×
NEW
133
        self.endMoveRows()
×
134

UNCOV
135
        return True
×
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