Source code for qconcurrency.models

#!/usr/bin/env python
"""
Name :          qconcurrency/models.py
Created :       Apr 14, 2017
Author :        Will Pittman
Contact :       willjpittman@gmail.com
________________________________________________________________________________
Description :   Generic models, and interfaces for models to be used
                in various Qt `View` widgets (ex: QTableView, QListView, QTableView, QComboBox, ...)
________________________________________________________________________________
"""
#builtin
from __future__          import unicode_literals
from __future__          import absolute_import
from __future__          import division
from __future__          import print_function
from collections         import Iterable, MutableMapping
#external
from Qt                  import QtGui, QtCore
import six
#internal

__all__ = [
    'DictModel',
    'DictModelRow',
]


#!TODO: implement the other QStandardItemModel methods (insertRow, ...)
#!      taking special care to handle self._data
#!      (to keep IPython happy)

#!TODO: test using delattr unecessary, (and potentially harmful)
#!      QStandardItemModel methods (like appendRow, setItem, ...)

#!TODO: validation based on `hierarchy`, preventing nesting below defined
#!TODO: validation of column-names when setting columnvals

[docs]class DictModel( QtGui.QStandardItemModel ): """ Customized python interface for :py:obj:`QtGui.QStandardItemModel` so that it's values, and nested tables can be accessed like a python dictionary. Example: Simple Example: .. code-block:: bash | _id | firstname | lastname | username | |========================================| | 101 | luke | skywalker | lukes | | 102 | leia | skywalker | leias | |========================================| .. code-block:: python model = DictModel( columns=('firstname','lastname','username') ) model.add_row( 101, columnvals = { 'firstname':'luke' , 'lastname' :'skywalker' , 'username' :'lukes' , } ) userId = 101 print( model[userId].column('firstname') ) >>> 'luke' print( model[userId].columnvals() ) >>> {'_id':101, 'firstname':'luke', 'lastname':'skywalker', 'username':'lukes'} Nested-Table Example: .. code-block:: bash |=============| | _id | class | # level: 'jedi_class' |=============| | 101 | sith | | |===========================================| | | _id | firstname | lastname | username | # level: 'user' | |===========================================| | | 56 | Darth | Vader | anakins | | | 57 | Darth | Maul | darthm | | |===========================================| | | | 102 | jedi | | |===========================================| | | _id | firstname | lastname | username | # level: 'user' | |===========================================| | | 58 | Mace | Windu | macew | | | 59 | Ben | Kenobi | benk | | |===========================================| | | |=============| .. code-block:: python model = DictModel( hierarchy = ('jedi_class','user'), columns = { 'jedi_class': ('class'), 'user': ('firstname','lastname','username') }, ) sith_row = model.add_row( 101, {'class':'sith'} ) jedi_row = model.add_row( 102, {'class':'jedi'} ) sith_row.add_child( 56, {'firstname':'Darth', 'lastname':'Vader', 'username':'anakins'} ) sith_row.add_child( 57, {'firstname':'Darth', 'lastname':'Maul', 'username':'darthm'} ) jediclassId = 101 userId = 56 print( model[jediclassId][userId].column('username') ) >>> 'anakins' print( model[jediclassId].level() ) >>> 'jedi_class' print( model[jediclassId][userId].level() ) >>> 'user' :py:obj:`qconcurrency.models.DictModel` column datatypes .. code-block:: bash |===============================================| | _id | columnA | columnB | |===============================================| | DictModelRow | QStandardItem | QStandardItem | | |===============================================| | | _id | columnA | columnB | | |===============================================| | | DictModelRow | QStandardItem | QStandardItem | | |===============================================| | | |===============================================| """
[docs] def __init__(self, columns, hierarchy=None ): """ Args: columns (list, dict): Defines the available columns for the table/tree this :py:obj:`QtGui.QStandardItemModel` (the `key`, generally referring to the databaseId, is always the first column) If `hierarchy` argument is set, you have two options: * This can be a list of column-names, that will be created in all levels of nested table. * This can be a dictionary in the form of ``{'level_name':(column,column,column,...), ...}`` that indicates specific-columns for each level of table-nesting. If `hierarchy` is not set, this must be a list of column-names, and they will be applicable to any level of table-nesting. .. code-block:: python { 'jedi_class': ('class',), 'user': ('firstname','lastname'), } hierarchy (dict, optional): ``(ex: ('department_type','department') )`` A list that labels what type of data is stored at each level of table-nesting in this :py:obj:`qconcurrency.models.DictModel`. Each item indicates another level of nesting. .. code-block:: python hierarchy = ('jedi_class','user'), """ QtGui.QStandardItemModel.__init__(self) # Attributes self._defaultcolumnvals = {} # all columns for new rows are initialized as ``None`` self._columns = None # either a list of columns, or a dict of hierarchy-keys and their columns self._hierarchy = None # either ``None``, or a list, indicating the level of each self._data = {} # unfortunately, if an item has a dict interface # and has assignments within a context-manager, # IPython tries to save/restore the values when it # is destroyed. # # This means we need a real-dict (or something # to fake it) in order to cleanly use # within IPython. So we are now keeping # 2x references to the data. # Validation # ========== # If imposing hierarchy restrictions if hierarchy: self._hierarchy = hierarchy if isinstance( columns, MutableMapping ): if not set(hierarchy).issubset( set(columns.keys()) ): raise RuntimeError(( '`columns` argument is missing keys represented in`hierarchy` \n' 'columns: %s \n' 'hierarchy: %s \n' ) % (repr(columns.keys()), repr(hierarchy)) ) # so that hierarchy can always be handled the same, # create `columns` as a dict, if a list was passed elif hasattr( columns, Iterable ): new_columns = {} for level in hierarchy: new_columns[ level ] = columns[:] columns = new_columns else: raise RuntimeError( 'When `hierarchy` argument is set, `columns` must be either: \n' ' * a list of columns (applicable to all hierarchy levels) \n' ' * a dict of hierarchy-keys, and the columns associated with them \n' ) for level in hierarchy: self._defaultcolumnvals[ level ] = {} for key in columns[level]: self._defaultcolumnvals[ level ][ key ] = None # If not imposing hierarchy restrictions else: if isinstance( columns, MutableMapping ): raise RuntimeError( 'When `hierarchy` argument is *not* set, `columns` should always \n' 'be a list of column-names. This set of columns will be reused by all \n' 'levels of nested tables. ' ) for key in columns: self._defaultcolumnvals[ key ] = None self._columns = columns self._hierarchy = hierarchy
[docs] def add_row(self, key, columnvals=None ): """ Adds a new (toplevel) row to this DictModel, henceforth referred to by the key `key`. Args: key (obj): Key is the id you will use to refer to this object. Generally it will be a databaseId. This object must be hashable. columnvals (dict, optional): Optionally, you may provide a dictionary of column-val assignments (appropriate to this item's table-level). All columns, not assigned in `columnvals` will be initialized with a value of ''. Returns: :py:obj:`qconcurrency.models.DictModelRow` """ set_columnvals = self._defaultcolumnvals.copy() if self._hierarchy: set_columnvals = set_columnvals[ self._hierarchy[0] ] if columnvals: set_columnvals.update( columnvals ) item = DictModelRow( parent=self, key=key, columnvals=set_columnvals) # NOTE: this step should not be necessary, # but it seems to be... self.setItem( self.rowCount()-1, 0, item ) self._data[ str(key) ] = item return item
[docs] def columns(self, level=None ): """ Returns the columns for a particular level of nested-table within this :py:obj:`qconcurrency.models.DictModel`. Args: level (obj): ``( ex: 'jedi_class', 0 )`` If a `hierarchy` was assigned to this :py:obj:`qconcurrency.models.DictModel`, this can be a label from it, or an integer indicating the level-of-nesting. Otherwise, this will be an integer indicating the level-of-nesting (and it will be ignored). Returns: .. code-block:: python ('id','firstname','lastname','username', ...) """ if self._hierarchy: if level == None: raise RuntimeError( 'This `qconcurrency.models.DictModel` was created with different columns at ' 'different levels. You\'ll need to provide the `level` you are ' 'interested in to get the column-list ' ) if level in self._columns: columns = list(self._columns[ level ][:]) columns.insert( 0, 'id' ) return columns elif isinstance( level, int ) and isinstance( self._hierarchy, Iterable): if level <= len(self._hierarchy): i = 0 for key in self._hierarchy: if i == level: columns = list(self._columns[ key ][:]) columns.insert( 0, 'id' ) return columns i +=1 raise KeyError('unknown level: %s' % level ) else: columns = list(self._columns[:]) columns.insert( 0, 'id' ) return columns
[docs] def column_index(self, level=None, column=None ): """ Returns the column-index for a specific columnname at a specific level. Args: level (obj): ``( ex: 'jedi_class', 0 )`` If a `hierarchy` was assigned to this :py:obj:`qconcurrency.models.DictModel`, this can be a label from it, or an integer indicating the level-of-nesting. Otherwise, this will be an integer indicating the level-of-nesting (and it will be ignored). Returns: .. code-block:: python 3 # a column-index """ if self._hierarchy: if level == None: raise RuntimeError( 'This `qconcurrency.models.DictModel` was created with different columns at ' 'different levels. You\'ll need to provide the `level` you are ' 'interested in to get the column-list ' ) if level in self._columns: return self._columns[ level ].index( column ) +1 elif isinstance( level, int ) and isinstance( self._hierarchy, Iterable ): if level <= len(self._hierarchy): i = 0 for key in self._hierarchy: if i == level: return self._columns[ key ].index( column ) +1 i +=1 raise KeyError('unknown level: %s' % level ) else: return self._columns.index( column ) +1
[docs] def default_columnvals(self, level=None ): """ Returns the default-columnvals for a particular level of nested-table. See :py:meth:`qconcurrency.models.DictModelRow.level` Args: level (obj): If a `hierarchy` was assigned to this :py:obj:`qconcurrency.models.DictModel`, this will be a label from it. Otherwise, this will be an integer indicating the level-of-nesting (and it will be ignored). Returns: .. code-block:: python { 'firstname': None, 'lastname': None, ... } """ if self._hierarchy: if level in self._defaultcolumnvals: return self._defaultcolumnvals[ level ] elif isinstance( level, int ): if level <= len(self._defaultcolumnvals): i = 0 for key in self._defaultcolumnvals: if i == level: return self._defaultcolumnvals[ key ] i +=1 raise KeyError('unknown level: %s' % level ) else: return self._defaultcolumnvals
[docs] def hierarchy(self): """ Returns the model's hierarchy tuple (if one has been assigned in :py:obj:`qconcurrency.models.DictModel.__init__`) Returns: .. code-block:: python ('jedi_class', 'user') # if assigned a hierarchy None # if no hierarchy is assigned """ return self._hierarchy
def _get_rowitem(self, key): """ Returns the item in the first column of this :py:obj:`QtGui.QStandardItemModel` for the row with the key indicated by `key`. Args: key (obj): A key assigned to a row within this Model. Generally, this would be a database-Id. Returns: QtGui.QStandardItem """ for i in range(self.rowCount()): if self.item(i,0).text() == str(key): return self.item(i,0) raise KeyError( 'no row has the key "%s"' % key ) def _get_colindex(self, level, column): """ Returns the column-index for a column within this :py:obj:`QtGui.QStandardItemModel` by it's name. Args: column (str): ``(ex: 'name' )`` Any item from the :py:meth:`__init__` argument `columns`. Returns: .. code-block:: python 4 # integer, representing the 0-based index of this column # in the table Raises: KeyError: if column does not exist in table """ if self._hierarchy: if level == None: raise RuntimeError( 'This `qconcurrency.models.DictModel` was created with different columns at ' 'different levels. You\'ll need to provide the `level` you are ' 'interested in to get the column-list ' ) if level in self._columns: return self._columns[ level ].index( column ) +1 elif isinstance( level, int ) and isinstance( self._hierarchy, Iterable ): if level <= len(self._hierarchy): i = 0 for key in self._hierarchy: if i == level: return self._columns[ key ].index( column ) +1 i +=1 raise KeyError('unknown level: %s' % level ) else: return self._columns.index(column) +1 raise KeyError( 'Column "%s" does not exist in this `qconcurrency.models.DictModel` columns: %s' % ( column, str(self._columns) ) ) def keys(self): """ Returns list containing keys for every row that has been added to this :py:obj:`qconcurrency.models.DictModel` s root Returns: .. code-block:: python [ 1, 2, 3, 5, 8, ... ] """ return self._data.keys()
[docs] def removeRow(self, key): self._data.pop( str(key) ) # row is gone. that is all we care about try: modelitem = self._get_rowitem( key ) return QtGui.QStandardItemModel.removeRow( self, modelitem.row() ) except( KeyError ): return
[docs] def takeRow(self, key): return self.removeRow( str(key) )
def __getitem__(self, key): """ Returns a :py:obj:`qconcurrency.models.DictModelRow` object representing a row from this :py:obj:`qconcurrency.models.DictModel`. """ return self._data[str(key)] def __delitem__(self, key): """ Wraps :py:meth:`removeRow` """ self.removeRow( key ) def __contains__(self, item): """ Returns True/False if a row with `key` exists in :py:obj:`QtWidgets.QStandardItemModel` """ return str(item) in self._data def __len__(self): """ Wraps `self._data.__len__` """ return len(self._data) def __iter__(self): """ Allows iteration over Ids in DictModel. """ return iter(self._data)
[docs] def has_key(self, k): """ Wraps `self._data.has_key` """ return self._data.has_key(k)
[docs] def keys(self): """ Lists `key` value for every row in the :py:obj:`QtWidgets.QStandardItemModel` """ return self._data.keys()
[docs] def values(self): """ Lists :py:obj:`DictModelRow` objects for every row in the :py:obj:`QtWidgets.QStandardItemModel` """ return self._data.values()
[docs] def items(self): """ Lists a tuple with the `key` and :py:obj:`DictModelRow` objects for every row in the :py:obj:`QtWidgets.QStandardItemModel` """ return self._data.items()
[docs] def clear(self): """ Removes all items from :py:obj:`QtGui.QStandardItemModel` """ self._data = {} QtGui.QStandardItemModel.clear(self)
[docs]class DictModelRow( QtGui.QStandardItem ): """ A DictModelRow is a :py:obj:`QtGui.QStandardItem` that holds an item's key (usually database-Id) within a :py:obj:`qconcurrency.models.DictModel`. It is always added to a :py:obj:`qconcurrency.models.DictModel` at the column-index ``0``. When setting columnvals, they are added to the same parent :py:obj:`qconcurrency.models.DictModel` or :py:obj:`qconcurrency.models.DictModelRow`, but at different column-indexes. Example: .. code-block:: bash ===== ========| DictModelRow _id | class | # level: 'jedi_class' | ===== ========| +---------> 101 | sith | | |============================================| | | _id | firstname | lastname | username | # level: 'user' | |============================================| +-------------> 56 | Darth | Vader | anakins | +-------------> 57 | Darth | Maul | darthm | |============================================| /\\ /\\ /\\ | | | QtGui.QStandardItem ----+------------+-------------+ """
[docs] def __init__(self, parent, key, columnvals=None ): """ Args: parent (QtGui.QStandardItem, QtGui.QStandardItemModel ): Another QStandardItem that has already been added to the model, or a model itself. It will be used to access the model's info, and this widget will be added to it. key (obj): A hashable python object that will be used to represent this object's databaseId. columnvals (dict, optional): A dictionary of columns, and assignments to store in the view. """ QtGui.QStandardItem.__init__(self, str(key)) if not isinstance( parent, QtGui.QStandardItemModel ): if not parent.model(): raise RuntimeError( '`parent` %s QStandardItem must have already been added to a QStandardItemModel' % repr(parent) ) self._key = key self._level = None # if `hierarchy` argument was set in `DictModel`, this will be # a label indicating the type of information this # table represents. # # otherwise, this will be an incremented integer # (starting from 0) # append this item to the parent's list of children if isinstance( parent, QtGui.QStandardItemModel ): if parent.hierarchy(): self._level = parent.hierarchy()[0] else: self._level = 0 parent.setItem( parent.rowCount(), 0, self ) else: hierarchy = parent.model().hierarchy() if hierarchy: index = hierarchy.index( parent.level() )+1 self._level = hierarchy[ index ] else: self._level = parent.level() +1 parent.setChild( parent.rowCount(), 0, self ) self.setText( str(key) ) default_columnvals = self.model().default_columnvals(self._level) self.set_columnvals( default_columnvals ) if columnvals: self.set_columnvals( columnvals )
def __getitem__(self, key): return self._get_child_row(key)
[docs] def add_child(self, key, columnvals=None ): """ Adds a new row to this DictModel, at a new level of nesting henceforth referred to by the key `key`. Example: .. code-block:: bash |==============| | _id | column | |==============| | 100 | 'A' | # add_child( 102, {'column':'A1'} ) | |==============| | | _id | column | # added child: model[100][102] | |==============| | | 102 | 'A1' | | |==============| | | | 101 | 'B' | |==============| Args: key (obj): Key is the id you will use to refer to this object. Generally it will be a databaseId. This object must be hashable. columnvals (dict, optional): Optionally, you may provide a dictionary of column-val assignments (appropriate to this item's table-level) as determined by the `columns` argument to :py:meth:`qconcurrency.models.DictModel.__init__` Returns: :py:obj:`qconcurrency.models.DictModelRow` See Also: * :py:meth:`qconcurrency.models.DictModelRow.add_row` * :py:meth:`qconcurrency.models.DictModel.add_row` """ item = DictModelRow( parent=self, key=key, columnvals=columnvals ) return item
[docs] def add_row(self, key, columnvals=None ): """ Adds a new row to this DictModel, at the same level of nesting henceforth referred to by the key `key`. Example: .. code-block:: bash |==============| | _id | column | |==============| | 100 | 'A' | # add_row( 102, {'column':'C'} ) | 101 | 'B' | | 102 | 'C' | # added row: model[102] |==============| Args: key (obj): Key is the id you will use to refer to this object. Generally it will be a databaseId. This object must be hashable. columnvals (dict, optional): Optionally, you may provide a dictionary of column-val assignments (appropriate to this item's table-level) as determined by the `columns` argument to :py:meth:`qconcurrency.models.DictModel.__init__` Returns: :py:obj:`qconcurrency.models.DictModelRow` See Also: * :py:meth:`qconcurrency.models.DictModelRow.add_row` * :py:meth:`qconcurrency.models.DictModel.add_row` """ if self.parent(): item = DictModelRow( parent=self.parent(), key=key, columnvals=columnvals ) else: item = DictModelRow( parent=self.model(), key=key, columnvals=columnvals ) return item
[docs] def set_columnvals(self, columnvals ): """ Set columnvals on a key of this :py:obj:`qconcurrency.models.DictModel` """ # validation if self.model() is None: raise RuntimeError('Cannot set columnvals until item has been added to a model') columns = self.model().columns( self._level ) # set columnvals for i in range(len(columns)): column = columns[i] if column in columnvals: if columnvals[column] == None: columnvals[column] = '' if self.parent() is not None: self.parent().setChild( self.index().row(), # row i, # column QtGui.QStandardItem( str(columnvals[column]) ) # item ) else: self.model().setItem( self.index().row(), # row i, # column QtGui.QStandardItem( str(columnvals[column]) ) # item )
[docs] def columnvals(self): """ Returns a dictionary of this item's columnvals from the Model. A column `_id` will be added to the list of columns, which will be the `key` value of this row. """ columnvals = {} columns = self.model().columns(self._level) for i in range(len(columns)): column = columns[i] # nested-modelitem if self.parent() is not None: modelitem = self.parent().child( self.row(), i+1 ) if modelitem is not None: columnvals[ column ] = modelitem.text() else: raise RuntimeError( 'item at level "%s" in column "%s" (%s,%s) is None. Expected QtCore.QStandardItem' % ( self._level, column, self.row(), i+1) ) # root-modelitems else: modelitem = self.model().item( self.row(), i+1 ) if modelitem is not None: columnvals[ column ] = modelitem.text() else: raise RuntimeError( 'item at level "%s" in column "%s" (%s,%s) is None. Expected QtCore.QStandardItem' % ( self._level, column, self.row(), i+1) ) columnvals['_id'] = self._key return columnvals
[docs] def columnval(self, name): """ Retrieve a single column-value only. """ if name == '_id': if self.parent() is not None: return self.parent().child( self.row(), 0 ).text() else: return self.model().item( self.row(), 0 ).text() columns = self.model().columns(self._level) for i in range(len(columns)): column = columns[i] if column == name: if self.parent() is not None: modelitem = self.parent().child( self.row(), i ) if modelitem: return modelitem.text() else: modelitem = self.model().item( self.row(), i ) if modelitem: return modelitem.text() raise KeyError( 'Unable to find a column named: "%s" in %s' % (name, repr(columns)) )
[docs] def columnitem(self, name): """ Returns the sibling-widget representing one of the columnvals """ sibling_index = self.index().sibling( self.index().row(), self._get_colindex( name ) ) return self.model().itemFromIndex( sibling_index )
[docs] def level(self): """ Returns either a label (if :py:meth:`qconcurrency.models.DictModel.__init__` was passed a `hierarchy` argument), or an integer representing the nesting-depth. Either way, level is used to indicate the level-of-nesting of the table that this item is in. """ return self._level
[docs] def delete(self): """ Removes this *row* from the model. """ if self.parent() is not None: self.parent().removeRow( self.id() ) else: self.model().removeRow( self.id() )
def _get_sibling_row(self, key): """ Returns a sibling with a different key at the same level. Example: .. code-block:: bash ---------------------------------------------------- key | name | path | ---------------------------------------------------- 100 | 'mnt' | '/mnt' | 100.1 | 'mntusb' | '/mnt/usb' | 100.1.a | 'mntusbbackup' | '/mnt/usb/backup' | 100.2 | 'mntcd' | '/mnt/cd' | | | | 200 | 'home' | '/home' | 200.1 | 'will' | '/home/will' | In the above diagram representing the :py:obj:`QStandardItemModel`, from `100.1` you would be able retrieve `100.2`. """ if self.parent() == None: for i in range(self.model().rowCount()): if self.model().item(i,0).text() == str(key): return self.model().item(i,0) else: for i in range(self.parent().rowCount()): if self.parent().child(i,0).text() == str(key): return self.parent().child(i,0) raise KeyError( 'Unable to find key %s in table containing %s' % (key, repr(self)) ) def _get_child_row(self, key): """ Returns a child with a particular key. Example: .. code-block:: bash ---------------------------------------------------- key | name | path | ---------------------------------------------------- 100 | 'mnt' | '/mnt' | 100.1 | 'mntusb' | '/mnt/usb' | 100.1.a | 'mntusbbackup' | '/mnt/usb/backup' | 100.2 | 'mntcd' | '/mnt/cd' | | | | 200 | 'home' | '/home' | 200.1 | 'will' | '/home/will' | In the above diagram representing the :py:obj:`QStandardItemModel`, from `100.1` you would be able retrieve `100.1.a`. """ if not self.rowCount(): raise RuntimeError( '%s has no children. Cannot retrieve child at key %s' % (repr(self), key) ) for i in range(self.rowCount()): if self.child(i,0).text() == str(key): return self.child(i,0) raise KeyError( 'Cannot find child identified by key "%s" in %s' % (key,repr(self)) ) def _get_colindex(self, column): return self.model()._get_colindex( self.level(), column )
[docs] def keys(self): """ Returns list containig keys for every child-row that has been added to this :py:obj:`qconcurrency.models.DictModelRow` """ keys = [] for i in range(self.rowCount()): keys.append( self.child(i,0).text() ) return keys
[docs] def id(self): """ Returns the `key` this row represents. (It's value depends on the value passed to :py:meth:`qconcurrency.models.DictModelRow.add_row` or :py:meth:`qconcurrency.models.DictModelRow.add_child` ). """ return self._key
if __name__ == '__main__': from qconcurrency import QApplication from Qt import QtWidgets import sys def test_simple(): with QApplication(): model = DictModel( columns=('a','b','c') ) # add toplevel rows model.add_row( 100, columnvals={'a':'AAA', 'b':'BBB'} ) model.add_row( 200, columnvals={'a':'ZZZ', 'b':'XXX'} ) print( model[100].columnvals() ) print( model[200].columnvals() ) # add child-rows (and nested children) model[100].add_child( 10, columnvals={'c':'CCC'} ) model[100][10].add_row( 11 ) model[100][10].add_row( 12 ) model[100][10].add_child( 1 , columnvals={'c':'DDD'} ) print( model[100][10].columnvals() ) print( model[100][10][1].columnvals() ) # add model to tree (so it is visible) tree = QtWidgets.QTreeView() tree.setModel( model ) tree.show() def test_hierarchy_fixedcols(): with QApplication(): model = QtGui.QStandardItemModel() model = DictModel( hierarchy = ('jedi_class','user'), columns = {'jedi_class':['class'], 'user':('username','firstname','lastname')} ) model.add_row(10, columnvals={'class':'sith'} ) model.add_row(11, columnvals={'class':'jedi'} ) model[10].add_child( 101, columnvals={'username':'anakins', 'firstname':'anakin', 'lastname':'skywalker'} ) model[10].add_child( 102, columnvals={'username':'epalpatine'} ) model[10].add_row( 12, columnvals={'class':'other'} ) jediclassId = 10 userId = 101 print( model[jediclassId][userId].columnvals() ) # add model to tree (so it is visible) tree = QtWidgets.QTreeView() tree.setModel( model ) tree.show() def runtests(): #test_simple() test_hierarchy_fixedcols() runtests()