Advertisement
fernly

Final table-sorting test-bed code

Mar 24th, 2015
234
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 15.51 KB | None | 0 0
  1. import random
  2. import time
  3. from sortedcontainers import SortedDict
  4.  
  5. # These functions are used to populate the database with
  6. # values that are kinda-sorta like the ones in my app.
  7.  
  8. def xdi(B,N,M):
  9.     '''
  10.            eXponential Distribution of Integers, XDI
  11.    Return a list of length N of integers with max value M, from an exponential
  12.    distribution with beta B.
  13.    '''
  14.     M = int(M) # just in case
  15.     # pre-allocate the list rather than building it with .append()
  16.     r = [0]*N
  17.     for j in range(N) :
  18.         r[j] = int( random.expovariate(B) ) % M
  19.     return r
  20.  
  21. RWTALPH = 'aeiouåáïbcdfghjklmnpqrstvwxyz_BCDFGHJKLMNPQRSTVWZ'
  22. def rwt(N):
  23.     '''
  24.            Random Word-like Token, RWT
  25.    Return a word-like token comprising N Latin-1 letters with an
  26.    emphasis on vowels.
  27.    '''
  28.     ii = xdi( 0.1, N, len(RWTALPH)-1 )
  29.     return ''.join( [ RWTALPH[i] for i in ii ] )
  30.  
  31. def rft():
  32.     '''
  33.            Random Flag Token
  34.    Return an 8-char token composed mostly of dashes with a sprinkle of Xs,
  35.    e.g. "---X--X-" or "XX------".
  36.  
  37.    '''
  38.     ii = xdi( 2, 8, 2 )
  39.     return ''.join( ['-X'[i] for i in ii ] )
  40.  
  41. #
  42. # Define the database. All this code is out here at the module level --
  43. # instead of inside the Table Model class -- because in the real app, the
  44. # database is managed in a separate module entirely.
  45. #
  46. # The database comprises a single SortedDict, DATABASE, in which the keys are
  47. # rwt(8) "word" tokens, and the values are tuples comprising ( word, int in
  48. # 0-99, rft() flag string). (Yes this is redundant in having the word as both
  49. # key and value but it simplifies coding the data() method of the model.)
  50. #
  51. DATABASE = SortedDict()
  52. NUMBER_OF_ROWS = 10000 # how many rows to create
  53. #
  54. # The contents are accessed via a valuesView object, VALUES_BY_ROW. This can
  55. # be indexed, so VALUES_BY_ROW[J] is the values for the Jth entry in the
  56. # ascending order of the dict keys. VALUES_BY_ROW[J][2] is the column-2 data
  57. # from row J (the flag string column).
  58. #
  59. VALUES_BY_ROW = None # valuesview here
  60. #
  61. # The database is ordered using the Python native default sort order, which
  62. # is almost certainly not what the data view wants. In order to sort it
  63. # correctly (i.e. locale-aware and with or without respecting case) the view
  64. # will impose a sort order using get_column_vector() below.
  65. #
  66. # In order to provide the indirection required for sorting, access to
  67. # VALUES_BY_ROW is by way of a sort vector of indices. As many as 6 such
  68. # vectors, one for each column for each sort order, are stored in a two
  69. # lists, SORT_UP_VECTORS and SORT_DOWN_VECTORS. When the database is
  70. # initialized, none of these exist. Thus the view must call get_column_vector
  71. # after initializing the database.
  72. #
  73. SORT_UP_DICTS = [ None, None, None ]
  74. SORT_UP_VECTORS = [ None, None, None ]
  75. SORT_DOWN_VECTORS = [ None, None, None ]
  76.  
  77. # Clear the database, called when the model initializes and before Refresh
  78.  
  79. def clear_the_db() :
  80.     global DATABASE, VALUES_BY_ROW, SORT_UP_VECTORS, SORT_DOWN_VECTORS
  81.     DATABASE.clear()
  82.     VALUES_BY_ROW = None
  83.     SORT_UP_DICTS = [ None, None, None ]
  84.     SORT_UP_VECTORS = [ None, None, None ]
  85.     SORT_DOWN_VECTORS = [ None, None, None ]
  86.  
  87. # Rebuild the database by repopulating it with phony data. This emulates what
  88. # worddata.py does when it scans the book and tabulates its vocabulary.
  89.  
  90. def rebuild_the_db():
  91.     global DATABASE, VALUES_BY_ROW, NUMBER_OF_ROWS
  92.     # Throw everything away. In the real app, worddata doesn't throw everything away,
  93.     # it only clears the counts in column 1.
  94.     clear_the_db()
  95.     # Create values for column 1, small random integers. Use the length
  96.     # of column 1 to control the loop.
  97.     column_1 = xdi( 0.05, NUMBER_OF_ROWS, 100 )
  98.     # Populate the database with rows of stuff
  99.     for k in column_1 :
  100.         w = rwt(8) # a random "word"
  101.         f = rft()  # a random "flag" string
  102.         DATABASE[ w ] = ( w, k, f )
  103.     # Create the indexable view of the data
  104.     VALUES_BY_ROW = DATABASE.values()
  105.  
  106. #
  107. # On request from the table model, return a sort vector for a given column
  108. # (0, 1 or 2) based on a given sort order and key function.
  109. #
  110. # 1. If we have a vector for that column and sort order, just return it.
  111. #
  112. # 2. If we do not have a vector for ascending order on this column, build a
  113. # dict for accessing the values of DATABASE in ascending sequence over that
  114. # column. Place the sortedDict in SORT_UP_DICTS[col].
  115. #
  116. # 3. If the request is for an ascending order, return that.
  117. #
  118. # 4. Make a vector for descending order by applying reversed() to the
  119. # ascending order dict and place it in SORT_DOWN_VECTORS[col], and return it.
  120. #
  121. # We build the ascending vector as follows. Make a SortedDict in which each
  122. # key is the concatenation of the column value for a row (which may have
  123. # dups) and the primary key for the same row. Use the key_func passed by the
  124. # caller. The combination is unique and sorts properly.
  125. #
  126. # The value associated to each key is the index of that row in DATABASE.
  127. # Because this is a SortedDict, the values taken in sequence produce the
  128. # DATABASE rows in column-value order.
  129. #
  130. # This list of functions extracts the key for a vector for each column.
  131. # Column 0, just the "word"; col 1, nnnnnPRIMARYKEY; col 2 XXXXXXXXPRIMARYKEY
  132. #
  133. KEY_GETTERS = [
  134.     lambda j : VALUES_BY_ROW[j][0],
  135.     lambda j : '{:05}{}'.format( VALUES_BY_ROW[j][1], VALUES_BY_ROW[j][0] ),
  136.     lambda j : '{}{}'.format( VALUES_BY_ROW[j][2], VALUES_BY_ROW[j][0] )
  137.     ]
  138. def get_column_vector( col, order, key_func = None, filter_func = None ):
  139.     global DATABASE, VALUES_BY_ROW, SORT_UP_DICTS, SORT_UP_VECTORS, SORT_DOWN_VECTORS
  140.     if filter_func is None :
  141.         # return a sort vector for the whole database, cached if we have one.
  142.         vector = SORT_UP_VECTORS[ col ] if order == Qt.AscendingOrder \
  143.             else SORT_DOWN_VECTORS[ col ]
  144.         if vector is None : # we need to make one
  145.             if SORT_UP_VECTORS[ col ] is None : # we have no up-vector
  146.                 getter_func = KEY_GETTERS[ col ]
  147.                 sort_dict = SortedDict( key_func )
  148.                 for j in range( len(DATABASE) ) :
  149.                     k = getter_func( j )
  150.                     sort_dict[ k ] = j
  151.                 # Cache the whole-database dict for later.
  152.                 SORT_UP_DICTS[ col ] = sort_dict
  153.                 # Cache the values-view of it for use by the view
  154.                 SORT_UP_VECTORS[ col ] = sort_dict.values()
  155.             # We have a sort-up vector, or was it sort-down we need?
  156.             vector = SORT_UP_VECTORS[ col ] # be optimistic
  157.             if order == Qt.DescendingOrder : # ok, make the reverse
  158.                 SORT_DOWN_VECTORS[ col ] = [ j for j in reversed( SORT_UP_VECTORS[ col ] ) ]
  159.                 vector = SORT_DOWN_VECTORS [ col ]
  160.     else :
  161.         # Build a filtered vector for the requested column, order and filter.
  162.         getter_func = KEY_GETTERS[ col ]
  163.         sort_dict = SortedDict( key_func )
  164.         for j in range( len(DATABASE) ) :
  165.             if filter_func( *VALUES_BY_ROW[j] ) :
  166.                 k = getter_func( j )
  167.                 sort_dict[ k ] = j
  168.         vector = sort_dict.values()
  169.         if order == Qt.DescendingOrder : # convert the up-vector to a down one
  170.             vector = [ j for j in reversed( vector ) ]
  171.     return vector
  172.  
  173. # Define the Table Model, instrumented to count certain calls
  174.  
  175. from PyQt5.QtCore import Qt, QAbstractTableModel, pyqtSignal
  176.  
  177. import natsort
  178.  
  179. class Model( QAbstractTableModel ):
  180.     tableSorted = pyqtSignal()
  181.  
  182.     def __init__ ( self, parent=None ) :
  183.         super().__init__( parent )
  184.         clear_the_db()
  185.         self.access_counts = [0, 0, 0]
  186.         self.sort_order = Qt.AscendingOrder
  187.         self.sort_column = 0
  188.         #self.sort_key_func = natsort.natsort_keygen( alg = (natsort.ns.LOCALE | natsort.ns.IGNORECASE ) )
  189.         self.sort_key_func = natsort.natsort_keygen( alg = (natsort.ns.LOCALE  ) )
  190.         self.sort_vector = []
  191.         self.filter_func = None
  192.         self.sort_time = 0.0 # TIMING
  193.  
  194.     def rowCount( self, index ) :
  195.         global DATABASE
  196.         if index.isValid() : return 0
  197.         return len(self.sort_vector) # 0 until Refresh is called
  198.  
  199.     def columnCount( self, index ) :
  200.         if index.isValid() : return 0
  201.         return 3
  202.  
  203.     def data(self, index, role ) :
  204.         global VALUES_BY_ROW, DXX
  205.         if role != Qt.DisplayRole :
  206.             return None
  207.         row = index.row()
  208.         col = index.column()
  209.         self.access_counts[col] += 1
  210.         sort_row = self.sort_vector[ row ]
  211.         return VALUES_BY_ROW[ sort_row ][ col ]
  212.  
  213.     def clear_counts( self ) :
  214.         self.access_counts = [0, 0, 0]
  215.  
  216.     def counts( self ) :
  217.         return list( self.access_counts )
  218.  
  219.     # method to let external code initiate a sort without knowing about
  220.     # how we store the sort column and order.
  221.     def do_sort( self ) :
  222.         self.sort( self.sort_column, self.sort_order )
  223.  
  224.     # reimplement QAbstractItemModel.sort() which receives two arguments,
  225.     # the column number to sort, and the sort order, Qt.AscendingOrder
  226.     # or Qt.DescendingOrder. Then emit layoutChanged to repaint the view.
  227.  
  228.     def sort( self, col, order ) :
  229.         t0 = time.process_time() # TIMING
  230.         self.sort_column = col
  231.         self.sort_order = order
  232.         self.sort_vector = get_column_vector( col, order, self.sort_key_func, self.filter_func )
  233.         self.layoutChanged.emit()
  234.         self.sort_time = time.process_time() - t0    # TIMING
  235.         self.tableSorted.emit()    # TIMING
  236.  
  237.     # Override endResetModel() to make sure to refresh the sort
  238.     # when there is new data.
  239.  
  240.     def endResetModel( self ) :
  241.         super().endResetModel()
  242.         self.sort( self.sort_column, self.sort_order )
  243.  
  244. # Define the Table View, which at this point is quite minimal.
  245. # The View is instantiated from MainWindow, which also connects
  246. # the model to it.
  247.  
  248. from PyQt5.QtWidgets import QTableView
  249.  
  250. class View( QTableView ):
  251.     def __init__ ( self, parent=None ) :
  252.         super().__init__( parent )
  253.         self.setSortingEnabled( True )
  254.         self.sortByColumn( 0, Qt.AscendingOrder )
  255.  
  256. # Define the main window which is the visual face of this app.
  257.  
  258. from PyQt5.QtWidgets import (
  259.     QComboBox,
  260.     QLabel,
  261.     QMainWindow,
  262.     QPushButton,
  263.     QVBoxLayout,
  264.     QHBoxLayout,
  265.     QWidget
  266.     )
  267.  
  268. class Main( QMainWindow ) :
  269.     def __init__ ( self ) :
  270.         super().__init__( )
  271.         self.times = [0, 0, 0]
  272.         clear_the_db() # make sure to start with 0 rows
  273.         self._uic() # all the layout stuff out of line
  274.         self.refresh_button.clicked.connect( self.do_refresh )
  275.         self.table_model.tableSorted.connect( self.update_sort_time )
  276.         self.popup.activated.connect(self.set_filter)
  277.  
  278.     # Slot called when Refresh is clicked:
  279.     # * clear the counts
  280.     # * start model reset
  281.     # * rebuild the DATABASE, timing it
  282.     # * end the model reset, timing that
  283.     # * update the labels displaying call counts and times
  284.  
  285.     def do_refresh( self ) :
  286.         self.table_model.clear_counts()
  287.         self.table_model.beginResetModel()
  288.         self.times[0] = time.process_time()
  289.         rebuild_the_db()
  290.         self.times[1] = time.process_time()
  291.         self.table_model.endResetModel()
  292.         QApplication.processEvents()
  293.         self.times[2] = time.process_time()
  294.         self.update_labels()
  295.  
  296.     # Slot called when the popup combobox is activated. Input is
  297.     # the index of the chosen entry, 0=All. Set the filter and
  298.     # force a sort with the current column and order.
  299.     filters = [
  300.         None,
  301.         lambda w, c, f: f[0]=='X',
  302.         lambda w, c, f: f[1]=='X',
  303.         lambda w, c, f: f[2]=='X'
  304.         ]
  305.     def set_filter(self, choice) :
  306.         self.table_model.filter_func = self.filters[ choice ]
  307.         self.table_model.do_sort()
  308.  
  309.     # Slot called when a sort happens. Get the process time from
  310.     # the table model and put it in the sort_time_label.
  311.  
  312.     def update_sort_time( self ) :
  313.         self.sort_time_label.setText ('{:02.5f}'.format( self.table_model.sort_time  ) )
  314.         self.row_count.setText(str(len(self.table_model.sort_vector)) )
  315.  
  316.     def update_labels(self) :
  317.         [c0, c1, c2] = self.table_model.counts()
  318.         [t0, t1, t2] = self.times
  319.         self.c0_label.setText( str(c0) )
  320.         self.c1_label.setText( str(c1) )
  321.         self.c2_label.setText( str(c2) )
  322.         self.td_label.setText( '{:02.5f}'.format(t1-t0) )
  323.         self.tr_label.setText( '{:02.5f}'.format(t2-t1) )
  324.  
  325.     def _make_label( self, text='0' ) :
  326.         # just make a right-aligned label out of line
  327.         L = QLabel(text)
  328.         L.setAlignment( Qt.AlignRight | Qt.AlignVCenter )
  329.         return L
  330.  
  331.     def _uic( self ) :
  332.         # create the table
  333.         self.table_view = View( parent=self )
  334.         self.table_model = Model( parent=self )
  335.         self.table_view.setModel( self.table_model )
  336.         # create the refresh button, put it in an hbox by with the sort time
  337.         self.refresh_button = QPushButton( "Refresh" )
  338.         hb0 = QHBoxLayout()
  339.         hb0.addWidget(self.refresh_button, 0)
  340.         # create a combobox with 4 entries
  341.         self.popup = QComboBox()
  342.         self.popup.addItems( ['All','X..','.X.','..X'] )
  343.         hb0.addWidget( self.popup )
  344.         hb0.addStretch(1)
  345.         hb0.addWidget( self._make_label( 'sort time:'), 0  )
  346.         self.sort_time_label = self._make_label()
  347.         hb0.addWidget( self.sort_time_label )
  348.         hb0.addWidget( self._make_label( 'rows:'), 0 )
  349.         self.row_count = self._make_label()
  350.         hb0.addWidget( self.row_count )
  351.         # create a set of labels to display counts of
  352.         # entry to the model.data() method and times
  353.         self.c0_label = self._make_label() # display role calls to column 0
  354.         self.c1_label = self._make_label() # 1
  355.         self.c2_label = self._make_label() # 2
  356.         self.tr_label = self._make_label() # time to reset the model
  357.         self.td_label = self._make_label() # time to rebuild the db
  358.         # build the row of call numbers
  359.         hb1 = QHBoxLayout()
  360.         hb1.addStretch(1) # push this row to the right
  361.         hb1.addWidget( self._make_label( 'Display role calls col 0:' ) )
  362.         hb1.addWidget( self.c0_label )
  363.         hb1.addStretch(0)
  364.         hb1.addWidget( self._make_label( 'col 1:' ) )
  365.         hb1.addWidget( self.c1_label )
  366.         hb1.addStretch(0)
  367.         hb1.addWidget( self._make_label( 'col 2:' ) )
  368.         hb1.addWidget( self.c2_label )
  369.         # build the row of times
  370.         hb2 = QHBoxLayout()
  371.         hb2.addStretch(1)
  372.         hb2.addWidget( self._make_label( 'Seconds to build DB:' ) )
  373.         hb2.addWidget( self.td_label )
  374.         hb2.addStretch(0)
  375.         hb2.addWidget( self._make_label( 'to reset model:' ) )
  376.         hb2.addWidget( self.tr_label )
  377.         # stack up the central layout
  378.         vb = QVBoxLayout()
  379.         vb.addLayout( hb0, 0 )
  380.         vb.addWidget( self.table_view, 1 )
  381.         vb.addLayout( hb1, 0 )
  382.         vb.addLayout( hb2, 0 )
  383.         # put all that in a widget and make the widget our central layout
  384.         wij = QWidget()
  385.         wij.setLayout( vb )
  386.         wij.setMinimumSize( 500, 500 )
  387.         self.setCentralWidget( wij )
  388.  
  389.  
  390. if __name__ == '__main__' :
  391.     import sys
  392.     print(sys.argv)
  393.     try :
  394.         NUMBER_OF_ROWS = int( sys.argv[1] )
  395.     except Exception as whatever :
  396.         NUMBER_OF_ROWS = 10000
  397.     from PyQt5.QtWidgets import QApplication
  398.     the_app = QApplication(sys.argv)
  399.     main_window = Main()
  400.     main_window.show()
  401.     the_app.exec_()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement