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

Open-MSS / MSS / 15876623240

25 Jun 2025 12:41PM UTC coverage: 69.899% (+0.03%) from 69.872%
15876623240

Pull #2830

github

web-flow
Merge 412cf3942 into 001cc76b1
Pull Request #2830: added a special handling for path strings

8 of 12 new or added lines in 1 file covered. (66.67%)

15 existing lines in 2 files now uncovered.

14483 of 20720 relevant lines covered (69.9%)

0.7 hits per line

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

58.42
/mslib/support/qt_json_view/datatypes.py
1
from functools import partial
1✔
2
import re
1✔
3
import webbrowser
1✔
4

5
# (mss)
6
from PyQt5 import QtCore, QtGui, QtWidgets
1✔
7

8

9
TypeRole = QtCore.Qt.UserRole + 1
1✔
10

11

12
class DataType:
1✔
13
    """Base class for data types."""
14

15
    # (mss)
16
    COLOR = QtCore.Qt.black
1✔
17

18
    def matches(self, data):
1✔
19
        """Logic to define whether the given data matches this type."""
20
        raise NotImplementedError
×
21

22
    def next(self, model, data, parent):  # noqa: A003
1✔
23
        """Implement if this data type has to add child items to itself."""
24
        pass
1✔
25

26
    def actions(self, index):
1✔
27
        """Re-implement to return custom QActions."""
28
        return []
×
29

30
    def paint(self, painter, option, index):
1✔
31
        """Optionally re-implement for use by the delegate."""
32
        raise NotImplementedError
1✔
33

34
    def createEditor(self, parent, option, index):
1✔
35
        """Optionally re-implement for use by the delegate."""
36
        raise NotImplementedError
×
37

38
    def setModelData(self, editor, model, index):
1✔
39
        """Optionally re-implement for use by the delegate."""
40
        raise NotImplementedError
×
41

42
    def serialize(self, model, item, data, parent):
1✔
43
        """Serialize this data type."""
44
        value_item = parent.child(item.row(), 1)
1✔
45
        value = value_item.data(QtCore.Qt.DisplayRole)
1✔
46
        if isinstance(data, dict):
1✔
47
            key_item = parent.child(item.row(), 0)
1✔
48
            key = key_item.data(QtCore.Qt.DisplayRole)
1✔
49
            data[key] = value
1✔
50
        elif isinstance(data, list):
1✔
51
            data.append(value)
1✔
52

53
    def key_item(self, key, model, datatype=None, editable=True):
1✔
54
        """Create an item for the key column for this data type."""
55
        key_item = QtGui.QStandardItem(key)
1✔
56
        key_item.setData(datatype, TypeRole)
1✔
57
        key_item.setData(datatype.__class__.__name__, QtCore.Qt.ToolTipRole)
1✔
58
        key_item.setData(
1✔
59
            QtGui.QBrush(datatype.COLOR), QtCore.Qt.ForegroundRole)
60
        key_item.setFlags(
1✔
61
            QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
62
        if editable and model.editable_keys:
1✔
63
            key_item.setFlags(key_item.flags() | QtCore.Qt.ItemIsEditable)
1✔
64
        return key_item
1✔
65

66
    def value_item(self, value, model, key=None):
1✔
67
        """Create an item for the value column for this data type."""
68
        display_value = value
1✔
69
        item = QtGui.QStandardItem()
1✔
70
        item.setData(display_value, QtCore.Qt.DisplayRole)
1✔
71
        item.setData(value, QtCore.Qt.UserRole)
1✔
72
        item.setData(self, TypeRole)
1✔
73
        item.setData(QtGui.QBrush(self.COLOR), QtCore.Qt.ForegroundRole)
1✔
74
        item.setFlags(
1✔
75
            QtCore.Qt.ItemIsSelectable |
76
            QtCore.Qt.ItemIsEnabled)
77
        if model.editable_values:
1✔
78
            item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
1✔
79
        return item
1✔
80

81

82
# -----------------------------------------------------------------------------
83
# Default Types
84
# -----------------------------------------------------------------------------
85

86

87
class NoneType(DataType):
1✔
88
    """None"""
89

90
    def matches(self, data):
1✔
91
        return data is None
1✔
92

93
    def value_item(self, value, model, key=None):
1✔
94
        item = super(NoneType, self).value_item(value, model, key)
×
95
        item.setData('None', QtCore.Qt.DisplayRole)
×
96
        return item
×
97

98
    def serialize(self, model, item, data, parent):
1✔
99
        value_item = parent.child(item.row(), 1)
×
100
        value = value_item.data(QtCore.Qt.DisplayRole)
×
101
        value = value if value != 'None' else None
×
102
        if isinstance(data, dict):
×
103
            key_item = parent.child(item.row(), 0)
×
104
            key = key_item.data(QtCore.Qt.DisplayRole)
×
105
            data[key] = value
×
106
        elif isinstance(data, list):
×
107
            data.append(value)
×
108

109

110
class StrType(DataType):
1✔
111
    """Strings and unicodes"""
112

113
    def matches(self, data):
1✔
114
        # (mss)
115
        return isinstance(data, str)
1✔
116

117

118
class IntType(DataType):
1✔
119
    """Integers"""
120

121
    def matches(self, data):
1✔
122
        return isinstance(data, int) and not isinstance(data, bool)
1✔
123

124

125
class FloatType(DataType):
1✔
126
    """Floats"""
127

128
    def matches(self, data):
1✔
129
        return isinstance(data, float)
1✔
130

131

132
class BoolType(DataType):
1✔
133
    """Bools are displayed as checkable items with a check box."""
134

135
    def matches(self, data):
1✔
136
        return isinstance(data, bool)
1✔
137

138
    def value_item(self, value, model, key=None):
1✔
139
        item = super(BoolType, self).value_item(value, model, key)
1✔
140
        item.setCheckState(QtCore.Qt.Checked if value else QtCore.Qt.Unchecked)
1✔
141
        item.setData('', QtCore.Qt.DisplayRole)
1✔
142
        if model.editable_values:
1✔
143
            item.setFlags(
1✔
144
                item.flags() | QtCore.Qt.ItemIsEditable |
145
                QtCore.Qt.ItemIsUserCheckable)
146
        return item
1✔
147

148
    def serialize(self, model, item, data, parent):
1✔
149
        value_item = parent.child(item.row(), 1)
1✔
150
        value = value_item.checkState() == QtCore.Qt.Checked
1✔
151
        if isinstance(data, dict):
1✔
152
            key_item = parent.child(item.row(), 0)
1✔
153
            key = key_item.data(QtCore.Qt.DisplayRole)
1✔
154
            data[key] = value
1✔
155
        elif isinstance(data, list):
×
156
            data.append(value)
×
157

158

159
class ListType(DataType):
1✔
160
    """Lists"""
161

162
    def matches(self, data):
1✔
163
        return isinstance(data, list)
1✔
164

165
    def next(self, model, data, parent):  # noqa: A003
1✔
166
        for i, value in enumerate(data):
1✔
167
            type_ = match_type(value)
1✔
168
            key_item = self.key_item(
1✔
169
                str(i), datatype=type_, editable=False, model=model)
170
            value_item = type_.value_item(value, model=model, key=str(i))
1✔
171
            parent.appendRow([key_item, value_item])
1✔
172
            type_.next(model, data=value, parent=key_item)
1✔
173

174
    def value_item(self, value, model, key):
1✔
175
        item = QtGui.QStandardItem()
1✔
176
        item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
1✔
177
        return item
1✔
178

179
    def serialize(self, model, item, data, parent):
1✔
180
        key_item = parent.child(item.row(), 0)
1✔
181
        if key_item:
1✔
182
            if isinstance(data, dict):
1✔
183
                key = key_item.data(QtCore.Qt.DisplayRole)
1✔
184
                data[key] = []
1✔
185
                data = data[key]
1✔
186
            elif isinstance(data, list):
1✔
187
                new_data = []
1✔
188
                data.append(new_data)
1✔
189
                data = new_data
1✔
190
        for row in range(item.rowCount()):
1✔
191
            child_item = item.child(row, 0)
1✔
192
            type_ = child_item.data(TypeRole)
1✔
193
            type_.serialize(
1✔
194
                model=self, item=child_item, data=data, parent=item)
195

196

197
class DictType(DataType):
1✔
198
    """Dictionaries"""
199

200
    def matches(self, data):
1✔
201
        return isinstance(data, dict)
1✔
202

203
    def next(self, model, data, parent):  # noqa: A003
1✔
204
        for key, value in data.items():
1✔
205
            type_ = match_type(value)
1✔
206
            key_item = self.key_item(key, datatype=type_, model=model)
1✔
207
            value_item = type_.value_item(value, model, key)
1✔
208
            parent.appendRow([key_item, value_item])
1✔
209
            type_.next(model, data=value, parent=key_item)
1✔
210

211
    def value_item(self, value, model, key):
1✔
212
        item = QtGui.QStandardItem()
1✔
213
        item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
1✔
214
        return item
1✔
215

216
    def serialize(self, model, item, data, parent):
1✔
217
        key_item = parent.child(item.row(), 0)
1✔
218
        if key_item:
1✔
219
            if isinstance(data, dict):
1✔
220
                key = key_item.data(QtCore.Qt.DisplayRole)
1✔
221
                data[key] = {}
1✔
222
                data = data[key]
1✔
223
            elif isinstance(data, list):
×
224
                new_data = {}
×
225
                data.append(new_data)
×
226
                data = new_data
×
227
        for row in range(item.rowCount()):
1✔
228
            child_item = item.child(row, 0)
1✔
229
            type_ = child_item.data(TypeRole)
1✔
230
            type_.serialize(model=self, item=child_item, data=data, parent=item)
1✔
231

232

233
# -----------------------------------------------------------------------------
234
# Derived Types
235
# -----------------------------------------------------------------------------
236

237

238
class RangeType(DataType):
1✔
239
    """A range, shown as three spinboxes next to each other.
240

241
    A range is defined as a dict with start, end and step keys.
242
    It supports both floats and ints.
243
    """
244

245
    KEYS = ['start', 'end', 'step']
1✔
246

247
    def matches(self, data):
1✔
248
        if isinstance(data, dict) and len(data) == 3:
1✔
249
            if all([True if k in self.KEYS else False for k in data.keys()]):
×
250
                return True
×
251
        return False
1✔
252

253
    def paint(self, painter, option, index):
1✔
254
        data = index.data(QtCore.Qt.UserRole)
×
255

256
        painter.save()
×
257

258
        painter.setPen(QtGui.QPen(index.data(QtCore.Qt.ForegroundRole).color()))
×
259
        metrics = painter.fontMetrics()
×
260
        spinbox_option = QtWidgets.QStyleOptionSpinBox()
×
261
        start_rect = QtCore.QRect(option.rect)
×
262
        start_rect.setWidth(int(start_rect.width() / 3.0))
×
263
        spinbox_option.rect = start_rect
×
264
        spinbox_option.frame = True
×
265
        spinbox_option.state = option.state
×
266
        spinbox_option.buttonSymbols = QtWidgets.QAbstractSpinBox.NoButtons
×
267
        for i, key in enumerate(self.KEYS):
×
268
            if i > 0:
×
269
                spinbox_option.rect.adjust(
×
270
                    spinbox_option.rect.width(), 0,
271
                    spinbox_option.rect.width(), 0)
272
            QtWidgets.QApplication.style().drawComplexControl(
×
273
                QtWidgets.QStyle.CC_SpinBox, spinbox_option, painter)
274
            value = str(data[key])
×
275
            value_rect = QtCore.QRectF(
×
276
                spinbox_option.rect.adjusted(6, 1, -2, -2))
277
            value = metrics.elidedText(
×
278
                value, QtCore.Qt.ElideRight, value_rect.width() - 20)
279
            painter.drawText(value_rect, value)
×
280

281
        painter.restore()
×
282

283
    def createEditor(self, parent, option, index):
1✔
284
        data = index.data(QtCore.Qt.UserRole)
×
285
        wid = QtWidgets.QWidget(parent)
×
286
        wid.setLayout(QtWidgets.QHBoxLayout(parent))
×
287
        wid.layout().setContentsMargins(0, 0, 0, 0)
×
288
        wid.layout().setSpacing(0)
×
289

290
        start = data['start']
×
291
        end = data['end']
×
292
        step = data['step']
×
293

294
        if isinstance(start, float):
×
295
            start_spinbox = QtWidgets.QDoubleSpinBox(wid)
×
296
        else:
297
            start_spinbox = QtWidgets.QSpinBox(wid)
×
298

299
        if isinstance(end, float):
×
300
            end_spinbox = QtWidgets.QDoubleSpinBox(wid)
×
301
        else:
302
            end_spinbox = QtWidgets.QSpinBox(wid)
×
303

304
        if isinstance(step, float):
×
305
            step_spinbox = QtWidgets.QDoubleSpinBox(wid)
×
306
        else:
307
            step_spinbox = QtWidgets.QSpinBox(wid)
×
308

309
        start_spinbox.setRange(-16777215, 16777215)
×
310
        end_spinbox.setRange(-16777215, 16777215)
×
311
        step_spinbox.setRange(-16777215, 16777215)
×
312
        start_spinbox.setValue(start)
×
313
        end_spinbox.setValue(end)
×
314
        step_spinbox.setValue(step)
×
315
        wid.layout().addWidget(start_spinbox)
×
316
        wid.layout().addWidget(end_spinbox)
×
317
        wid.layout().addWidget(step_spinbox)
×
318
        return wid
×
319

320
    def setModelData(self, editor, model, index):
1✔
321
        if isinstance(model, QtWidgets.QAbstractProxyModel):
×
322
            index = model.mapToSource(index)
×
323
            model = model.sourceModel()
×
324
        data = index.data(QtCore.Qt.UserRole)
×
325
        data['start'] = editor.layout().itemAt(0).widget().value()
×
326
        data['end'] = editor.layout().itemAt(1).widget().value()
×
327
        data['step'] = editor.layout().itemAt(2).widget().value()
×
328
        model.itemFromIndex(index).setData(data, QtCore.Qt.UserRole)
×
329

330
    def value_item(self, value, model, key=None):
1✔
331
        """Item representing a value."""
332
        value_item = super(RangeType, self).value_item(None, model, key)
×
333
        value_item.setData(value, QtCore.Qt.UserRole)
×
334
        return value_item
×
335

336
    def serialize(self, model, item, data, parent):
1✔
337
        value_item = parent.child(item.row(), 1)
×
338
        value = value_item.data(QtCore.Qt.UserRole)
×
339
        if isinstance(data, dict):
×
340
            key_item = parent.child(item.row(), 0)
×
341
            key = key_item.data(QtCore.Qt.DisplayRole)
×
342
            data[key] = value
×
343
        elif isinstance(data, list):
×
344
            data.append(value)
×
345

346

347
class UrlType(DataType):
1✔
348
    """Provide a link to urls."""
349

350
    REGEX = re.compile(r'(?:https?):\/\/|(?:file):\/\/')
1✔
351

352
    def matches(self, data):
1✔
353
        # (mss)
354
        if isinstance(data, str):
1✔
355
            if self.REGEX.match(data) is not None:
1✔
356
                return True
1✔
357
        return False
1✔
358

359
    def actions(self, index):
1✔
360
        explore = QtWidgets.QAction('Explore ...', None)
×
361
        explore.triggered.connect(
×
362
            partial(webbrowser.open, index.data(QtCore.Qt.DisplayRole)))
363
        return [explore]
×
364

365

366
class FilepathType(DataType):
1✔
367
    """Files and paths can be opened."""
368

369
    REGEX = re.compile(r'(\/.*)|([A-Z]:\\.*)')
1✔
370

371
    def matches(self, data):
1✔
372
        # (mss)
373
        if isinstance(data, str):
1✔
374
            if self.REGEX.match(data) is not None:
1✔
375
                return True
1✔
376
        return False
1✔
377

378
    def actions(self, index):
1✔
NEW
379
        explore = QtWidgets.QAction('Explore ...', None)
×
NEW
380
        path = index.data(QtCore.Qt.DisplayRole)
×
NEW
381
        explore.triggered.connect(partial(webbrowser.open, path))
×
NEW
382
        return [explore]
×
383

384

385
class ChoicesType(DataType):
1✔
386
    """A combobox that allows for a number of choices.
387

388
    The data has to be a dict with a value and a choices key.
389
    {
390
        "value": "A",
391
        "choices": ["A", "B", "C"]
392
    }
393
    """
394

395
    KEYS = ['value', 'choices']
1✔
396

397
    def matches(self, data):
1✔
398
        if isinstance(data, dict) and len(data) == 2:
1✔
399
            if all([True if k in self.KEYS else False for k in data.keys()]):
1✔
UNCOV
400
                return True
×
401
        return False
1✔
402

403
    def createEditor(self, parent, option, index):
1✔
UNCOV
404
        data = index.data(QtCore.Qt.UserRole)
×
UNCOV
405
        cbx = QtWidgets.QComboBox(parent)
×
UNCOV
406
        cbx.addItems([str(d) for d in data['choices']])
×
UNCOV
407
        cbx.setCurrentIndex(cbx.findText(str(data['value'])))
×
UNCOV
408
        return cbx
×
409

410
    def setModelData(self, editor, model, index):
1✔
UNCOV
411
        if isinstance(model, QtWidgets.QAbstractProxyModel):
×
UNCOV
412
            index = model.mapToSource(index)
×
UNCOV
413
            model = model.sourceModel()
×
UNCOV
414
        data = index.data(QtCore.Qt.UserRole)
×
UNCOV
415
        data['value'] = data['choices'][editor.currentIndex()]
×
UNCOV
416
        model.itemFromIndex(index).setData(data['value'], QtCore.Qt.DisplayRole)
×
417
        model.itemFromIndex(index).setData(data, QtCore.Qt.UserRole)
×
418

419
    def value_item(self, value, model, key=None):
1✔
420
        """Item representing a value."""
421
        value_item = super(ChoicesType, self).value_item(value['value'], model, key)
×
422
        value_item.setData(value, QtCore.Qt.UserRole)
×
423
        return value_item
×
424

425
    def serialize(self, model, item, data, parent):
1✔
UNCOV
426
        value_item = parent.child(item.row(), 1)
×
UNCOV
427
        value = value_item.data(QtCore.Qt.UserRole)
×
428
        if isinstance(data, dict):
×
429
            key_item = parent.child(item.row(), 0)
×
430
            key = key_item.data(QtCore.Qt.DisplayRole)
×
431
            data[key] = value
×
432
        elif isinstance(data, list):
×
433
            data.append(value)
×
434

435

436
# Add any custom DataType to this list
437
#
438
DATA_TYPES = [
1✔
439
    NoneType(),
440
    UrlType(),
441
    FilepathType(),
442
    StrType(),
443
    IntType(),
444
    FloatType(),
445
    BoolType(),
446
    ListType(),
447
    RangeType(),
448
    ChoicesType(),
449
    DictType()
450
]
451

452

453
def match_type(data):
1✔
454
    """Try to match the given data object to a DataType"""
455
    for type_ in DATA_TYPES:
1✔
456
        if type_.matches(data):
1✔
457
            return type_
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