2021-05-18 07:27:23 +12:00
|
|
|
"""Python adaptation of https://github.com/dridk/QJsonModel
|
|
|
|
|
|
|
|
Supports Python 2 and 3 with PySide, PySide2, PyQt4 or PyQt5.
|
|
|
|
Requires https://github.com/mottosso/Qt.py
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
Use it like you would the C++ version.
|
|
|
|
|
|
|
|
>>> import qjsonmodel
|
|
|
|
>>> model = qjsonmodel.QJsonModel()
|
|
|
|
>>> model.load({"key": "value"})
|
|
|
|
|
|
|
|
Test:
|
|
|
|
Run the provided example to sanity check your Python,
|
|
|
|
dependencies and Qt binding.
|
|
|
|
|
|
|
|
$ python qjsonmodel.py
|
|
|
|
|
|
|
|
Changes:
|
|
|
|
This module differs from the C++ version in the following ways.
|
|
|
|
|
|
|
|
1. Setters and getters are replaced by Python properties
|
|
|
|
2. Objects are sorted by default, disabled via load(sort=False)
|
|
|
|
3. load() takes a Python dictionary as opposed to
|
|
|
|
a string or file handle.
|
|
|
|
|
|
|
|
- To load from a string, use built-in `json.loads()`
|
|
|
|
>>> import json
|
|
|
|
>>> document = json.loads("{'key': 'value'}")
|
|
|
|
>>> model.load(document)
|
|
|
|
|
|
|
|
- To load from a file, use `with open(fname)`
|
|
|
|
>>> import json
|
|
|
|
>>> with open("file.json") as f:
|
|
|
|
... document = json.load(f)
|
|
|
|
... model.load(document)
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
from PyQt5 import QtCore, QtWidgets
|
|
|
|
|
|
|
|
|
|
|
|
class QJsonTreeItem(object):
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
self._parent = parent
|
|
|
|
|
|
|
|
self._key = ""
|
|
|
|
self._value = ""
|
|
|
|
self._type = None
|
|
|
|
self._children = list()
|
|
|
|
|
|
|
|
def appendChild(self, item):
|
|
|
|
self._children.append(item)
|
|
|
|
|
|
|
|
def child(self, row):
|
|
|
|
return self._children[row]
|
|
|
|
|
|
|
|
def parent(self):
|
|
|
|
return self._parent
|
|
|
|
|
|
|
|
def childCount(self):
|
|
|
|
return len(self._children)
|
|
|
|
|
|
|
|
def row(self):
|
|
|
|
return (
|
|
|
|
self._parent._children.index(self)
|
|
|
|
if self._parent else 0
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def key(self):
|
|
|
|
return self._key
|
|
|
|
|
|
|
|
@key.setter
|
|
|
|
def key(self, key):
|
|
|
|
self._key = key
|
|
|
|
|
|
|
|
@property
|
|
|
|
def value(self):
|
|
|
|
return self._value
|
|
|
|
|
|
|
|
@value.setter
|
|
|
|
def value(self, value):
|
|
|
|
self._value = value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def type(self):
|
|
|
|
return self._type
|
|
|
|
|
|
|
|
@type.setter
|
|
|
|
def type(self, typ):
|
|
|
|
self._type = typ
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def load(self, value, parent=None, sort=True):
|
|
|
|
rootItem = QJsonTreeItem(parent)
|
|
|
|
rootItem.key = "root"
|
|
|
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
items = (
|
|
|
|
sorted(value.items())
|
|
|
|
if sort else value.items()
|
|
|
|
)
|
|
|
|
|
|
|
|
for key, value in items:
|
|
|
|
child = self.load(value, rootItem)
|
|
|
|
child.key = key
|
|
|
|
child.type = type(value)
|
|
|
|
rootItem.appendChild(child)
|
|
|
|
|
|
|
|
elif isinstance(value, list):
|
|
|
|
for index, value in enumerate(value):
|
|
|
|
child = self.load(value, rootItem)
|
|
|
|
child.key = index
|
|
|
|
child.type = type(value)
|
|
|
|
rootItem.appendChild(child)
|
|
|
|
|
|
|
|
else:
|
|
|
|
rootItem.value = value
|
|
|
|
rootItem.type = type(value)
|
|
|
|
|
|
|
|
return rootItem
|
|
|
|
|
|
|
|
|
|
|
|
class QJsonModel(QtCore.QAbstractItemModel):
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
super(QJsonModel, self).__init__(parent)
|
|
|
|
|
|
|
|
self._rootItem = QJsonTreeItem()
|
|
|
|
self._headers = ("key", "value")
|
|
|
|
|
|
|
|
def clear(self):
|
|
|
|
self.load({})
|
|
|
|
|
|
|
|
def load(self, document):
|
|
|
|
"""Load from dictionary
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
document (dict): JSON-compatible dictionary
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
assert isinstance(document, (dict, list, tuple)), (
|
2021-05-21 23:40:10 +12:00
|
|
|
"`document` must be of dict, list or tuple, "
|
|
|
|
"not %s" % type(document)
|
2021-05-18 07:27:23 +12:00
|
|
|
)
|
|
|
|
|
|
|
|
self.beginResetModel()
|
|
|
|
|
|
|
|
self._rootItem = QJsonTreeItem.load(document)
|
|
|
|
self._rootItem.type = type(document)
|
|
|
|
|
|
|
|
self.endResetModel()
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def json(self, root=None):
|
|
|
|
"""Serialise model as JSON-compliant dictionary
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
root (QJsonTreeItem, optional): Serialise from here
|
|
|
|
defaults to the the top-level item
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
model as dict
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
root = root or self._rootItem
|
|
|
|
return self.genJson(root)
|
|
|
|
|
|
|
|
def data(self, index, role):
|
|
|
|
if not index.isValid():
|
|
|
|
return None
|
|
|
|
|
|
|
|
item = index.internalPointer()
|
|
|
|
|
|
|
|
if role == QtCore.Qt.DisplayRole:
|
|
|
|
if index.column() == 0:
|
|
|
|
return item.key
|
|
|
|
|
|
|
|
if index.column() == 1:
|
|
|
|
return item.value
|
|
|
|
|
|
|
|
elif role == QtCore.Qt.EditRole:
|
|
|
|
if index.column() == 1:
|
|
|
|
return item.value
|
|
|
|
|
|
|
|
def setData(self, index, value, role):
|
|
|
|
if role == QtCore.Qt.EditRole:
|
|
|
|
if index.column() == 1:
|
|
|
|
item = index.internalPointer()
|
|
|
|
item.value = str(value)
|
|
|
|
|
|
|
|
self.dataChanged.emit(index, index, [QtCore.Qt.EditRole])
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def headerData(self, section, orientation, role):
|
|
|
|
if role != QtCore.Qt.DisplayRole:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if orientation == QtCore.Qt.Horizontal:
|
|
|
|
return self._headers[section]
|
|
|
|
|
|
|
|
def index(self, row, column, parent=QtCore.QModelIndex()):
|
|
|
|
if not self.hasIndex(row, column, parent):
|
|
|
|
return QtCore.QModelIndex()
|
|
|
|
|
|
|
|
if not parent.isValid():
|
|
|
|
parentItem = self._rootItem
|
|
|
|
else:
|
|
|
|
parentItem = parent.internalPointer()
|
|
|
|
|
|
|
|
childItem = parentItem.child(row)
|
|
|
|
if childItem:
|
|
|
|
return self.createIndex(row, column, childItem)
|
|
|
|
else:
|
|
|
|
return QtCore.QModelIndex()
|
|
|
|
|
|
|
|
def parent(self, index):
|
|
|
|
if not index.isValid():
|
|
|
|
return QtCore.QModelIndex()
|
|
|
|
|
|
|
|
childItem = index.internalPointer()
|
|
|
|
parentItem = childItem.parent()
|
|
|
|
|
|
|
|
if parentItem == self._rootItem:
|
|
|
|
return QtCore.QModelIndex()
|
|
|
|
|
|
|
|
return self.createIndex(parentItem.row(), 0, parentItem)
|
|
|
|
|
|
|
|
def rowCount(self, parent=QtCore.QModelIndex()):
|
|
|
|
if parent.column() > 0:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
if not parent.isValid():
|
|
|
|
parentItem = self._rootItem
|
|
|
|
else:
|
|
|
|
parentItem = parent.internalPointer()
|
|
|
|
|
|
|
|
return parentItem.childCount()
|
|
|
|
|
|
|
|
def columnCount(self, parent=QtCore.QModelIndex()):
|
|
|
|
return 2
|
|
|
|
|
|
|
|
def flags(self, index):
|
|
|
|
flags = super(QJsonModel, self).flags(index)
|
|
|
|
|
|
|
|
if index.column() == 1:
|
|
|
|
return QtCore.Qt.ItemIsEditable | flags
|
|
|
|
else:
|
|
|
|
return flags
|
|
|
|
|
|
|
|
def genJson(self, item):
|
|
|
|
nchild = item.childCount()
|
|
|
|
|
|
|
|
if item.type is dict:
|
|
|
|
document = {}
|
|
|
|
for i in range(nchild):
|
|
|
|
ch = item.child(i)
|
|
|
|
document[ch.key] = self.genJson(ch)
|
|
|
|
return document
|
|
|
|
|
|
|
|
elif item.type == list:
|
|
|
|
document = []
|
|
|
|
for i in range(nchild):
|
|
|
|
ch = item.child(i)
|
|
|
|
document.append(self.genJson(ch))
|
|
|
|
return document
|
|
|
|
|
|
|
|
else:
|
|
|
|
return item.value
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
import sys
|
|
|
|
|
|
|
|
app = QtWidgets.QApplication(sys.argv)
|
|
|
|
view = QtWidgets.QTreeView()
|
|
|
|
model = QJsonModel()
|
|
|
|
|
|
|
|
view.setModel(model)
|
|
|
|
|
|
|
|
document = json.loads("""\
|
|
|
|
{
|
|
|
|
"firstName": "John",
|
|
|
|
"lastName": "Smith",
|
|
|
|
"age": 25,
|
|
|
|
"address": {
|
|
|
|
"streetAddress": "21 2nd Street",
|
|
|
|
"city": "New York",
|
|
|
|
"state": "NY",
|
|
|
|
"postalCode": "10021"
|
|
|
|
},
|
|
|
|
"phoneNumber": [
|
|
|
|
{
|
|
|
|
"type": "home",
|
|
|
|
"number": "212 555-1234"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"type": "fax",
|
|
|
|
"number": "646 555-4567"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
""")
|
|
|
|
|
|
|
|
model.load(document)
|
|
|
|
model.clear()
|
|
|
|
model.load(document)
|
|
|
|
|
|
|
|
# Sanity check
|
|
|
|
assert (
|
2021-05-21 23:40:10 +12:00
|
|
|
json.dumps(model.json(), sort_keys=True) ==
|
|
|
|
json.dumps(document, sort_keys=True)
|
2021-05-18 07:27:23 +12:00
|
|
|
)
|
|
|
|
|
|
|
|
view.show()
|
|
|
|
view.resize(500, 300)
|
|
|
|
app.exec_()
|