Browse Source

fix(TTktable): crash for empty tables (#481)

pull/482/head
Pier CeccoPierangioliEugenio 5 months ago committed by GitHub
parent
commit
33256b61d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/testing.yml
  2. 2
      libs/pyTermTk/TermTk/TTkAbstract/abstracttablemodel.py
  3. 16
      libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablemodellist.py
  4. 4
      libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablemodelsqlite3.py
  5. 28
      libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py
  6. 0
      tests/pytest/modelView/test_tablemodelcsv.py
  7. 278
      tests/pytest/modelView/test_tablemodellist.py
  8. 410
      tests/pytest/modelView/test_tablemodelsqlite3.py
  9. 690
      tests/pytest/modelView/test_tablewidget.py
  10. 4
      tests/t.ui/test.ui.032.table.12.py
  11. 25
      tests/t.ui/test.ui.032.table.13.alignment.02.overloading.py
  12. 25
      tests/t.ui/test.ui.032.table.13.alignment.03.mixin.py

2
.github/workflows/testing.yml

@ -36,7 +36,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v3

2
libs/pyTermTk/TermTk/TTkAbstract/abstracttablemodel.py

@ -183,7 +183,7 @@ class TTkAbstractTableModel():
'''
return _TTkModelIndexList(row,col,self)
def data(self, row:int, col:int) -> object:
def data(self, row:int, col:int) -> Any:
'''
Returns the data stored for the item referred to by the row/column.

16
libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablemodellist.py

@ -22,6 +22,8 @@
__all__=['TTkTableModelList']
from typing import Any
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkAbstract.abstracttablemodel import TTkAbstractTableModel, TTkModelIndex
@ -68,7 +70,7 @@ class TTkTableModelList(TTkAbstractTableModel):
:param indexes: the index labels, defaults to the line number.
:type indexes: list[str], optional
'''
self._data = self._dataOriginal = data if data else [['']]
self._data = self._dataOriginal = data if data else []
self._hheader = header if header else []
self._vheader = indexes if indexes else []
super().__init__()
@ -93,8 +95,12 @@ class TTkTableModelList(TTkAbstractTableModel):
rowId = self._data[row] ,
rowCb = lambda rid: self._data.index(rid) )
def data(self, row:int, col:int) -> None:
return self._data[row][col]
def data(self, row:int, col:int) -> Any:
if ( row < 0 or col <0 or
row >= len(self._data) or
col>=len(col_data:=self._data[row]) ):
return None
return col_data[col]
def setData(self, row:int, col:int, data:object) -> None:
self._data[row][col] = data
@ -146,13 +152,13 @@ class TTkTableModelList(TTkAbstractTableModel):
for _l in self._data:
_l[column:column+count] = []
# Signal: from (0, column) with size (all rows, all remaining columns from removal point)
self.dataChanged.emit((0,column),(self.rowCount(), self.columnCount() - column))
self.dataChanged.emit((0,column),(self.rowCount(), self.columnCount() - column + 1))
self.modelChanged.emit()
return True
def removeRows(self, row:int, count:int) -> bool:
self._data[row:row+count] = []
# Signal: from (row, 0) with size (all remaining rows from removal point, all columns)
self.dataChanged.emit((row,0),(self.rowCount() - row, self.columnCount()))
self.dataChanged.emit((row,0),(self.rowCount() - row + 1, self.columnCount()))
self.modelChanged.emit()
return True

4
libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablemodelsqlite3.py

@ -143,7 +143,7 @@ class TTkTableModelSQLite3(TTkAbstractTableModel):
f"SELECT {self._key} FROM {self._table} "
f"{self._sort} "
f"LIMIT 1 OFFSET {row}")
key=res.fetchone()[0]
key = None if not (_fetch:=res.fetchone()) else _fetch[0]
return _TTkModelIndexSQLite3(col=col,rowId=key,sqModel=self)
def data(self, row:int, col:int) -> Any:
@ -152,7 +152,7 @@ class TTkTableModelSQLite3(TTkAbstractTableModel):
f"SELECT {self._columns[col]} FROM {self._table} "
f"{self._sort} "
f"LIMIT 1 OFFSET {row}")
return res.fetchone()[0]
return None if not (_fetch:=res.fetchone()) else _fetch[0]
def setData(self, row:int, col:int, data:object) -> None:
with self._sqliteMutex:

28
libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py

@ -83,7 +83,9 @@ class _ClipboardTable(TTkString):
def data(self) -> list:
return self._data
def _toTTkString(self) -> str:
def _toTTkString(self) -> TTkString:
if not self._data:
return TTkString()
def _lineHeight(_line):
return max(len(str(_item[2]).split('\n')) for _item in _line)
ret = []
@ -446,11 +448,13 @@ class TTkTableWidget(TTkAbstractScrollView):
data = self._clipboard.text()
self.pasteEvent(data)
def pasteEvent(self, data:object):
def pasteEvent(self, data:object) -> bool:
row,col = self._currentPos if self._currentPos else (0,0)
if isinstance(data,_ClipboardTable):
rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
if not rows or not cols:
return
if isinstance(data,_ClipboardTable) and data.data():
dataList = []
linearData = [_item for _line in data.data() for _item in _line]
minx,maxx = min(_a:=[_item[1] for _item in linearData]),max(_a)
@ -561,8 +565,8 @@ class TTkTableWidget(TTkAbstractScrollView):
self._snapshotId = 0
rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
self._vHeaderSize = vhs = 1+max(len(self._tableModel.headerData(_p, TTkK.VERTICAL)) for _p in range(rows) )
self._hHeaderSize = hhs = 1
self._vHeaderSize = vhs = 0 if not rows else 1+max(len(self._tableModel.headerData(_p, TTkK.VERTICAL)) for _p in range(rows) )
self._hHeaderSize = hhs = 0 if not rows else 1
self.setPadding(hhs,0,vhs,0)
if self._showVSeparators:
self._colsPos = [(1+x)*11 for x in range(cols)]
@ -583,8 +587,8 @@ class TTkTableWidget(TTkAbstractScrollView):
showHH = self._horizontallHeader.isVisible()
hhs = self._hHeaderSize if showHH else 0
vhs = self._vHeaderSize if showVH else 0
w = vhs+self._colsPos[-1]+1
h = hhs+self._rowsPos[-1]+1
w = vhs+(self._colsPos[-1] if self._colsPos else 0)+1
h = hhs+(self._rowsPos[-1] if self._rowsPos else 0)+1
return w,h
def clearSelection(self) -> None:
@ -846,6 +850,8 @@ class TTkTableWidget(TTkAbstractScrollView):
:param width: its width
:type width: int
'''
if column < 0 or column >= len(self._colsPos):
return
i = column
prevPos = self._colsPos[i-1] if i>0 else -1
if self._showVSeparators:
@ -930,7 +936,7 @@ class TTkTableWidget(TTkAbstractScrollView):
cola,colb = max(0,col-30), min(col+30,cols)
else:
cola,colb = 0,cols
return max(_hei(i) for i in range(cola,colb))
return 0 if cola>=colb else max(_hei(i) for i in range(cola,colb))
@pyTTkSlot(int)
def resizeRowToContents(self, row:int) -> None:
@ -1512,6 +1518,8 @@ class TTkTableWidget(TTkAbstractScrollView):
rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
if not rows or not cols:
return
rp = self._rowsPos
cp = self._colsPos

0
tests/pytest/modelView/test_003_tablemodelcsv.py → tests/pytest/modelView/test_tablemodelcsv.py

278
tests/pytest/modelView/test_001_tablemodellist.py → tests/pytest/modelView/test_tablemodellist.py

@ -47,9 +47,9 @@ class TestTTkTableModelList:
"""Test default initialization"""
model = ttk.TTkTableModelList()
assert model.rowCount() == 1
assert model.columnCount() == 1
assert model.data(0, 0) == ''
assert model.rowCount() == 0
assert model.columnCount() == 0
assert model.data(0, 0) == None
def test_init_with_data(self):
"""Test initialization with data"""
@ -213,7 +213,7 @@ class TestTTkTableModelList:
def test_insert_rows(self):
"""Test insertRows method"""
model = ttk.TTkTableModelList(data=self.test_data.copy())
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_count = model.rowCount()
# Insert 2 rows at position 1
@ -222,7 +222,7 @@ class TestTTkTableModelList:
assert result == True
assert model.rowCount() == original_count + 2
# Check that None values were inserted
# Check that empty string values were inserted
assert model.data(1, 0) == ''
assert model.data(1, 1) == ''
assert model.data(1, 2) == ''
@ -235,16 +235,16 @@ class TestTTkTableModelList:
def test_insert_columns(self):
"""Test insertColumns method"""
model = ttk.TTkTableModelList(data=[row.copy() for row in self.test_data])
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_column_count = model.columnCount()
# Insert 2 columns at position 1
result = model.insertColumns(1, 2)
assert result == True # Implementation returns True
assert result == True
assert model.columnCount() == original_column_count + 2
# Check that None values were inserted at the right position
# Check that empty string values were inserted at the right position
assert model.data(0, 1) == '' # New column 1
assert model.data(0, 2) == '' # New column 2
assert model.data(0, 3) == 25 # Original column 1 shifted to position 3
@ -256,13 +256,13 @@ class TestTTkTableModelList:
def test_remove_columns(self):
"""Test removeColumns method"""
model = ttk.TTkTableModelList(data=[row.copy() for row in self.test_data])
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_column_count = model.columnCount()
# Remove 1 column at position 1 (Age column)
result = model.removeColumns(1, 1)
assert result == True # Changed from False to True
assert result == True
assert model.columnCount() == original_column_count - 1
# Check that the middle column was removed
@ -273,125 +273,223 @@ class TestTTkTableModelList:
def test_remove_rows(self):
"""Test removeRows method"""
model = ttk.TTkTableModelList(data=[row.copy() for row in self.test_data])
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_row_count = model.rowCount()
# Remove 1 row at position 1 (Bob's row)
result = model.removeRows(1, 1)
assert result == True # Changed from False to True
assert result == True
assert model.rowCount() == original_row_count - 1
# Check that the middle row was removed and data shifted
assert model.data(0, 0) == 'Alice' # First row unchanged
assert model.data(1, 0) == 'Charlie' # Charlie moved to position 1
def test_insert_columns_signal_emission(self):
"""Test that dataChanged signal is emitted when inserting columns"""
model = ttk.TTkTableModelList(data=[row.copy() for row in self.test_data])
def test_remove_rows_boundary_conditions(self):
"""Test removeRows method with boundary conditions and invalid ranges"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_row_count = model.rowCount()
# Mock the signal to track emissions
signal_mock = Mock()
@ttk.pyTTkSlot(tuple[int,int], tuple[int,int])
def _mock_signal(pos: tuple[int,int], size: tuple[int,int]):
signal_mock(pos, size)
model.dataChanged.connect(_mock_signal)
# Test removing rows at the edge (last row)
result = model.removeRows(2, 1) # Remove Charlie's row (index 2)
assert result == True
assert model.rowCount() == original_row_count - 1
assert model.data(0, 0) == 'Alice'
assert model.data(1, 0) == 'Bob'
# Charlie should be gone
assert model.data(2, 0) == None
# Reset for next test
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Test removing rows starting beyond valid range
try:
result = model.removeRows(5, 1) # Start beyond data bounds
# Should either return False or handle gracefully
# The behavior depends on implementation - some might allow it
except IndexError:
pass # This is acceptable behavior
# Test removing rows with count that goes beyond data bounds
result = model.removeRows(1, 10) # Remove more rows than exist
assert result == True
# Should remove rows 1 and 2 (Bob and Charlie), leaving only Alice
assert model.rowCount() == 1
assert model.data(0, 0) == 'Alice'
# Insert columns
model.insertColumns(1, 2)
# Reset for next test
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Verify signal was emitted with correct parameters
# Expected: start at (0,1), size is (rowCount, originalColumnCount-1)
signal_mock.assert_called_once_with((0, 1), (3, 4)) # 3 rows, 4 remaining columns
# Test removing rows with negative start index
try:
result = model.removeRows(-1, 1)
# Behavior may vary - could be handled gracefully or raise error
except (IndexError, ValueError):
pass # Expected for invalid index
def test_insert_rows_signal_emission(self):
"""Test that dataChanged signal is emitted when inserting rows"""
model = ttk.TTkTableModelList(data=[row.copy() for row in self.test_data])
# Test removing zero rows
result = model.removeRows(1, 0)
assert result == True
assert model.rowCount() == original_row_count # Should be unchanged
# Mock the signal to track emissions
signal_mock = Mock()
@ttk.pyTTkSlot(tuple[int,int], tuple[int,int])
def _mock_signal(pos: tuple[int,int], size: tuple[int,int]):
signal_mock(pos, size)
model.dataChanged.connect(_mock_signal)
def test_remove_columns_boundary_conditions(self):
"""Test removeColumns method with boundary conditions and invalid ranges"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_column_count = model.columnCount()
# Insert rows
model.insertRows(1, 2)
# Test removing columns at the edge (last column)
result = model.removeColumns(2, 1) # Remove Role column (index 2)
assert result == True
assert model.columnCount() == original_column_count - 1
assert model.data(0, 0) == 'Alice' # Name still there
assert model.data(0, 1) == 25 # Age still there
assert model.data(0, 2) == None # Role should be gone
# Reset for next test
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Test removing columns starting beyond valid range
try:
result = model.removeColumns(5, 1) # Start beyond data bounds
# Should either return False or handle gracefully
except IndexError:
pass # This is acceptable behavior
# Test removing columns with count that goes beyond data bounds
result = model.removeColumns(1, 10) # Remove more columns than exist
assert result == True
# Should remove columns 1 and 2 (Age and Role), leaving only Name
assert model.columnCount() == 1
assert model.data(0, 0) == 'Alice'
assert model.data(0, 1) == None # Age should be gone
# Verify signal was emitted
# Expected: start at (1,0), size is (originalRowCount-1, columnCount)
signal_mock.assert_called_once_with((1, 0), (4, 3)) # 4 remaining rows, 3 columns
# Reset for next test
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
def test_remove_columns_signal_emission(self):
"""Test that dataChanged signal is emitted when removing columns"""
model = ttk.TTkTableModelList(data=[row.copy() for row in self.test_data])
# Test removing columns with negative start index
try:
result = model.removeColumns(-1, 1)
# Behavior may vary - could be handled gracefully or raise error
except (IndexError, ValueError):
pass # Expected for invalid index
# Mock the signal to track emissions
signal_mock = Mock()
@ttk.pyTTkSlot(tuple[int,int], tuple[int,int])
def _mock_signal(pos: tuple[int,int], size: tuple[int,int]):
signal_mock(pos, size)
model.dataChanged.connect(_mock_signal)
# Test removing zero columns
result = model.removeColumns(1, 0)
assert result == True
assert model.columnCount() == original_column_count # Should be unchanged
def test_remove_operations_on_empty_model(self):
"""Test remove operations on empty model"""
model = ttk.TTkTableModelList(data=[])
# Test removing rows from empty model
try:
result = model.removeRows(0, 1)
# Should handle gracefully or return False
except IndexError:
pass # Acceptable behavior
# Test removing columns from empty model
try:
result = model.removeColumns(0, 1)
# Should handle gracefully or return False
except IndexError:
pass # Acceptable behavior
def test_remove_operations_on_single_cell_model(self):
"""Test remove operations on single cell model"""
model = ttk.TTkTableModelList(data=[['single']])
# Remove the only row
result = model.removeRows(0, 1)
assert result == True
assert model.rowCount() == 0
assert model.columnCount() == 0 # Column structure might remain
# Reset for column test
model = ttk.TTkTableModelList(data=[['single']])
# Remove columns
model.removeColumns(1, 1)
# Remove the only column
result = model.removeColumns(0, 1)
assert result == True
assert model.columnCount() == 0
assert model.rowCount() == 1 # Row structure might remain
def test_remove_all_rows(self):
"""Test removing all rows from model"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Remove all rows at once
result = model.removeRows(0, model.rowCount())
assert result == True
assert model.rowCount() == 0
# Verify signal was emitted
signal_mock.assert_called_once_with((0, 1), (3, 1))
def test_remove_all_columns(self):
"""Test removing all columns from model"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
def test_remove_rows_signal_emission(self):
"""Test that dataChanged signal is emitted when removing rows"""
model = ttk.TTkTableModelList(data=[row.copy() for row in self.test_data])
# Remove all columns at once
result = model.removeColumns(0, model.columnCount())
assert result == True
assert model.columnCount() == 0
def test_remove_operations_signal_emission_boundary_cases(self):
"""Test signal emission for boundary cases in remove operations"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Mock the signal to track emissions
signal_mock = Mock()
@ttk.pyTTkSlot(tuple[int,int], tuple[int,int])
def _mock_signal(pos: tuple[int,int], size: tuple[int,int]):
def _mock_signal(pos: tuple, size: tuple):
signal_mock(pos, size)
model.dataChanged.connect(_mock_signal)
# Remove rows
model.removeRows(1, 1)
# Remove last row
model.removeRows(2, 1)
# Should emit signal for the removal
assert signal_mock.call_count >= 1
# Reset mock and model
signal_mock.reset_mock()
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
model.dataChanged.connect(_mock_signal)
# Verify signal was emitted
signal_mock.assert_called_once_with((1, 0), (1, 3))
# Remove last column
model.removeColumns(2, 1)
# Should emit signal for the removal
assert signal_mock.call_count >= 1
def test_sort_updates_original_data(self):
"""Test that sorting affects _dataOriginal reference correctly"""
def test_remove_operations_preserve_data_integrity(self):
"""Test that remove operations preserve data integrity"""
original_data = [
['Charlie', 35, 'Manager'],
['Alice', 25, 'Engineer'],
['Bob', 30, 'Designer']
['A', 'B', 'C', 'D'],
['E', 'F', 'G', 'H'],
['I', 'J', 'K', 'L'],
['M', 'N', 'O', 'P']
]
model = ttk.TTkTableModelList(data=original_data)
# Sort by name
model.sort(0, ttk.TTkK.SortOrder.AscendingOrder)
sorted_data = model.modelList()
assert sorted_data[0][0] == 'Alice'
model = ttk.TTkTableModelList(data=[row[:] for row in original_data])
# Reset should go back to original order
model.sort(-1, ttk.TTkK.SortOrder.AscendingOrder)
reset_data = model.modelList()
assert reset_data == original_data
# Remove middle rows (1 and 2)
model.removeRows(1, 2)
def test_edge_case_empty_list_initialization(self):
"""Test edge case with empty list"""
model = ttk.TTkTableModelList(data=[])
# Verify remaining data is correct
assert model.rowCount() == 2
assert model.data(0, 0) == 'A' # First row unchanged
assert model.data(1, 0) == 'M' # Last row shifted up
# With empty data, it should create default [[']]
assert model.rowCount() == 1
assert model.columnCount() == 1
assert model.data(0, 0) == ''
# Reset and test column removal
model = ttk.TTkTableModelList(data=[row[:] for row in original_data])
def test_edge_case_none_data_initialization(self):
"""Test edge case with None data"""
model = ttk.TTkTableModelList(data=None)
# Remove middle columns (1 and 2)
model.removeColumns(1, 2)
# With None data, it should create default [['']]
assert model.rowCount() == 1
assert model.columnCount() == 1
assert model.data(0, 0) == ''
# Verify remaining data is correct
assert model.columnCount() == 2
assert model.data(0, 0) == 'A' # First column unchanged
assert model.data(0, 1) == 'D' # Last column shifted left
assert model.data(1, 0) == 'E'
assert model.data(1, 1) == 'H'
def test_integration_with_table_widget():
"""Integration test - verify TTkTableModelList works with table widgets"""

410
tests/pytest/modelView/test_002_tablemodelsqlite3.py → tests/pytest/modelView/test_tablemodelsqlite3.py

@ -751,49 +751,407 @@ class TestTTkTableModelSQLite3:
for j in range(model.columnCount()):
assert model.data(i, j) == original_data[i][j]
def test_empty_table_initialization(self):
"""Test initialization with an empty table"""
# Create empty table
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
cur.execute('''
CREATE TABLE empty_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
age INTEGER
)
''')
conn.commit()
conn.close()
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='empty_users')
assert model.rowCount() == 0
assert model.columnCount() == 2 # name, age (id is primary key)
# Test data access on empty table
assert model.data(0, 0) == None or model.data(0, 0) is None
# Test header data still works
assert model.headerData(0, ttk.TTkK.HORIZONTAL) == 'name'
assert model.headerData(1, ttk.TTkK.HORIZONTAL) == 'age'
def test_empty_table_operations(self):
"""Test operations on empty table"""
# Create empty table
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
cur.execute('''
CREATE TABLE empty_test (
id INTEGER PRIMARY KEY,
value TEXT
)
''')
conn.commit()
conn.close()
def test_integration_with_table_widget():
"""Integration test with TTkTable widget"""
# Create temporary database
temp_db = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
temp_db_path = temp_db.name
temp_db.close()
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='empty_test')
try:
# Set up database
conn = sqlite3.connect(temp_db_path)
# Test insert on empty table
result = model.insertRows(0, 1)
assert result == True
assert model.rowCount() == 1
# Test data setting on newly inserted row
result = model.setData(0, 0, 'test_value')
assert result == True
assert model.data(0, 0) == 'test_value'
# Test remove from single-row table
result = model.removeRows(0, 1)
assert result == True
assert model.rowCount() == 0
def test_empty_table_invalid_operations(self):
"""Test invalid operations on empty table"""
# Create empty table
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
cur.execute('''
CREATE TABLE test_table (
CREATE TABLE empty_invalid (
id INTEGER PRIMARY KEY,
data TEXT
)
''')
conn.commit()
conn.close()
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='empty_invalid')
# Test invalid remove operations on empty table
result = model.removeRows(0, 1)
assert result == False
result = model.removeRows(-1, 1)
assert result == False
result = model.removeRows(1, 1)
assert result == False
# Test invalid insert operations
result = model.insertRows(-1, 1)
assert result == False
result = model.insertRows(1, 1) # Beyond row count
assert result == False
def test_empty_table_sorting(self):
"""Test sorting operations on empty table"""
# Create empty table
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
cur.execute('''
CREATE TABLE empty_sort (
id INTEGER PRIMARY KEY,
name TEXT,
value INTEGER
)
''')
conn.commit()
conn.close()
cur.executemany('INSERT INTO test_table (name, value) VALUES (?, ?)', [
('Item1', 100),
('Item2', 200)
])
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='empty_sort')
# Test sorting on empty table (should not crash)
model.sort(0, ttk.TTkK.SortOrder.AscendingOrder)
assert model.rowCount() == 0
model.sort(1, ttk.TTkK.SortOrder.DescendingOrder)
assert model.rowCount() == 0
# Reset sort
model.sort(-1, ttk.TTkK.SortOrder.AscendingOrder)
assert model.rowCount() == 0
def test_remove_all_rows_from_populated_table(self):
"""Test removing all rows from a populated table"""
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='users')
initial_count = model.rowCount()
assert initial_count == 4
# Remove all rows at once
result = model.removeRows(0, initial_count)
assert result == True
assert model.rowCount() == 0
# Verify table is now empty in database
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
res = cur.execute("SELECT COUNT(*) FROM users")
assert res.fetchone()[0] == 0
conn.close()
# Test operations on now-empty table
result = model.removeRows(0, 1) # Should fail
assert result == False
# Test insert into now-empty table
result = model.insertRows(0, 1)
assert result == True
assert model.rowCount() == 1
def test_remove_all_rows_one_by_one(self):
"""Test removing all rows one by one"""
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='users')
initial_count = model.rowCount()
# Remove rows one by one from the beginning
for i in range(initial_count):
remaining_count = model.rowCount()
result = model.removeRows(0, 1)
assert result == True
assert model.rowCount() == remaining_count - 1
# Should now be empty
assert model.rowCount() == 0
# Verify in database
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
res = cur.execute("SELECT COUNT(*) FROM users")
assert res.fetchone()[0] == 0
conn.close()
def test_remove_all_rows_with_sorting(self):
"""Test removing all rows when table is sorted"""
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='users')
# Sort by name first
model.sort(0, ttk.TTkK.SortOrder.AscendingOrder)
initial_count = model.rowCount()
# Remove all rows
result = model.removeRows(0, initial_count)
assert result == True
assert model.rowCount() == 0
# Sorting should still work on empty table
model.sort(1, ttk.TTkK.SortOrder.DescendingOrder)
assert model.rowCount() == 0
def test_remove_rows_boundary_edge_cases(self):
"""Test boundary edge cases for removeRows"""
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='users')
row_count = model.rowCount()
# Test removing exactly the remaining count from position 0
result = model.removeRows(0, row_count)
assert result == True
assert model.rowCount() == 0
# Reset table for next test
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
test_data = [
('Alice', 25, 'Engineer'),
('Bob', 30, 'Designer'),
('Charlie', 35, 'Manager')
]
cur.executemany('INSERT INTO users (name, age, role) VALUES (?, ?, ?)', test_data)
conn.commit()
conn.close()
# Refresh model count
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='users')
# Test removing from last position with count that exceeds available
result = model.removeRows(2, 5) # Only 1 row at position 2, trying to remove 5
assert result == False
assert model.rowCount() == 3 # Should be unchanged
def test_insert_rows_into_empty_table(self):
"""Test inserting rows into various empty table configurations"""
# Create empty table with different column types
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
cur.execute('''
CREATE TABLE empty_mixed (
id INTEGER PRIMARY KEY,
name TEXT,
age INTEGER,
salary REAL,
active BOOLEAN
)
''')
conn.commit()
conn.close()
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='empty_mixed')
# Insert multiple rows into empty table
result = model.insertRows(0, 3)
assert result == True
assert model.rowCount() == 3
# Check default values based on column types
assert model.data(0, 0) == '' # TEXT -> empty string
assert model.data(0, 1) == 0 # INTEGER -> 0
assert model.data(0, 2) == 0.0 # REAL -> 0.0
assert model.data(0, 3) == 0 # BOOLEAN -> 0
def test_empty_table_index_operations(self):
"""Test index operations on empty table"""
# Create empty table
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
cur.execute('''
CREATE TABLE empty_index (
id INTEGER PRIMARY KEY,
value TEXT
)
''')
conn.commit()
conn.close()
# Test model with table widget
model = ttk.TTkTableModelSQLite3(fileName=temp_db_path, table='test_table')
table = ttk.TTkTable(tableModel=model)
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='empty_index')
# Test index creation on empty table (should handle gracefully)
try:
index = model.index(0, 0) # May raise exception or return invalid index
# If it doesn't raise an exception, the implementation handles it gracefully
except (sqlite3.Error, IndexError):
pass # Expected behavior for empty table
def test_table_emptied_then_repopulated(self):
"""Test table that is emptied and then repopulated"""
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='users')
# Verify initial state
assert model.rowCount() == 4
# Empty the table
model.removeRows(0, model.rowCount())
assert model.rowCount() == 0
# Basic integration tests
assert table.model() == model
# Repopulate with new data
model.insertRows(0, 2)
assert model.rowCount() == 2
assert model.columnCount() == 2
finally:
# Cleanup
if os.path.exists(temp_db_path):
os.unlink(temp_db_path)
# Set data for new rows
model.setData(0, 0, 'NewUser1')
model.setData(0, 1, 40)
model.setData(0, 2, 'Admin')
model.setData(1, 0, 'NewUser2')
model.setData(1, 1, 45)
model.setData(1, 2, 'Manager')
# Verify data integrity
assert model.data(0, 0) == 'NewUser1'
assert model.data(1, 0) == 'NewUser2'
# Test sorting on repopulated table
model.sort(0, ttk.TTkK.SortOrder.AscendingOrder)
assert model.data(0, 0) == 'NewUser1' # Should be first alphabetically
def test_concurrent_operations_on_empty_table(self):
"""Test concurrent operations on empty table (basic thread safety)"""
# Create empty table
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
cur.execute('''
CREATE TABLE empty_concurrent (
id INTEGER PRIMARY KEY,
value INTEGER
)
''')
conn.commit()
conn.close()
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='empty_concurrent')
# Simulate concurrent operations (basic test)
operations = [
lambda: model.insertRows(0, 1),
lambda: model.sort(0, ttk.TTkK.SortOrder.AscendingOrder),
lambda: model.rowCount(),
lambda: model.columnCount()
]
# Execute multiple operations - should not crash
for op in operations * 3: # Repeat operations
try:
result = op()
except Exception as e:
pytest.fail(f"Concurrent operation failed: {e}")
def test_large_batch_remove_operations(self):
"""Test removing large batches of rows"""
# First populate with more data
conn = sqlite3.connect(self.temp_db_path)
cur = conn.cursor()
# Add more test data
large_data = [(f'User{i}', 20+i, f'Role{i%3}') for i in range(50)]
cur.executemany('INSERT INTO users (name, age, role) VALUES (?, ?, ?)', large_data)
conn.commit()
conn.close()
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='users')
initial_count = model.rowCount()
assert initial_count > 50 # Should have original 4 + 50 new = 54
# Remove large batch from middle
result = model.removeRows(10, 30)
assert result == True
assert model.rowCount() == initial_count - 30
# Remove another large batch from beginning
result = model.removeRows(0, 15)
assert result == True
assert model.rowCount() == initial_count - 45
# Remove remaining rows
remaining = model.rowCount()
result = model.removeRows(0, remaining)
assert result == True
assert model.rowCount() == 0
def test_alternating_empty_and_populate_operations(self):
"""Test alternating between emptying and populating table"""
model = ttk.TTkTableModelSQLite3(fileName=self.temp_db_path, table='users')
for cycle in range(3): # Repeat cycle 3 times
# Start with some data
if model.rowCount() == 0:
model.insertRows(0, 2)
model.setData(0, 0, f'User{cycle}A')
model.setData(1, 0, f'User{cycle}B')
assert model.rowCount() >= 2
# Empty the table
model.removeRows(0, model.rowCount())
assert model.rowCount() == 0
# Insert different amount
insert_count = (cycle + 1) * 2
model.insertRows(0, insert_count)
assert model.rowCount() == insert_count
# Set some data
for i in range(insert_count):
model.setData(i, 0, f'Cycle{cycle}_User{i}')
if __name__ == '__main__':
pytest.main([__file__])
# Final cleanup
model.removeRows(0, model.rowCount())
assert model.rowCount() == 0

690
tests/pytest/modelView/test_tablewidget.py

@ -0,0 +1,690 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import sys
import pytest
from unittest.mock import Mock, MagicMock, patch
sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk'))
import TermTk as ttk
class TestTTkTableWidget:
"""Test cases for TTkTableWidget class"""
def setup_method(self):
"""Set up test fixtures before each test method."""
self.test_data = [
['Alice', 25, 'Engineer'],
['Bob', 30, 'Designer'],
['Charlie', 35, 'Manager']
]
self.header = ['Name', 'Age', 'Role']
self.indexes = ['Row1', 'Row2', 'Row3']
# Create a basic table model for testing
self.table_model = ttk.TTkTableModelList(
data=self.test_data,
header=self.header,
indexes=self.indexes
)
def test_init_default(self):
"""Test default initialization"""
widget = ttk.TTkTableWidget()
assert widget is not None
assert widget.model() is not None
assert isinstance(widget.model(), ttk.TTkAbstractTableModel)
# Default model should have 10x10 grid with empty strings
assert widget.rowCount() == 10
assert widget.columnCount() == 10
def test_init_with_model(self):
"""Test initialization with a table model"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
assert widget.model() == self.table_model
assert widget.rowCount() == 3
assert widget.columnCount() == 3
def test_init_with_separator_options(self):
"""Test initialization with separator visibility options"""
widget = ttk.TTkTableWidget(
vSeparator=False,
hSeparator=False
)
assert not widget.vSeparatorVisibility()
assert not widget.hSeparatorVisibility()
def test_init_with_header_options(self):
"""Test initialization with header visibility options"""
widget = ttk.TTkTableWidget(
vHeader=False,
hHeader=False
)
assert not widget.verticalHeader().isVisible()
assert not widget.horizontalHeader().isVisible()
def test_init_with_sorting_enabled(self):
"""Test initialization with sorting enabled"""
widget = ttk.TTkTableWidget(sortingEnabled=True)
assert widget.isSortingEnabled()
def test_init_with_data_padding(self):
"""Test initialization with custom data padding"""
widget = ttk.TTkTableWidget(dataPadding=3)
# Data padding is internal, but we can verify the widget was created
assert widget is not None
def test_model_getter_setter(self):
"""Test model getter and setter"""
widget = ttk.TTkTableWidget()
original_model = widget.model()
# Set new model
widget.setModel(self.table_model)
assert widget.model() == self.table_model
assert widget.model() != original_model
# Verify model data is accessible
assert widget.rowCount() == 3
assert widget.columnCount() == 3
def test_row_column_count(self):
"""Test row and column count methods"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
assert widget.rowCount() == 3
assert widget.columnCount() == 3
def test_current_row_column(self):
"""Test current row and column methods"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Initially should be at (0, 0) or (-1, -1) if no selection
current_row = widget.currentRow()
current_col = widget.currentColumn()
assert isinstance(current_row, int)
assert isinstance(current_col, int)
assert current_row >= -1
assert current_col >= -1
def test_header_views(self):
"""Test vertical and horizontal header views"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
v_header = widget.verticalHeader()
h_header = widget.horizontalHeader()
assert isinstance(v_header, ttk.TTkHeaderView)
assert isinstance(h_header, ttk.TTkHeaderView)
assert v_header.isVisible()
assert h_header.isVisible()
def test_separator_visibility(self):
"""Test separator visibility methods"""
widget = ttk.TTkTableWidget()
# Default should be visible
assert widget.hSeparatorVisibility()
assert widget.vSeparatorVisibility()
# Test setters
widget.setHSeparatorVisibility(False)
widget.setVSeparatorVisibility(False)
assert not widget.hSeparatorVisibility()
assert not widget.vSeparatorVisibility()
# Test setting back to visible
widget.setHSeparatorVisibility(True)
widget.setVSeparatorVisibility(True)
assert widget.hSeparatorVisibility()
assert widget.vSeparatorVisibility()
def test_sorting_functionality(self):
"""Test sorting enabled/disabled and sort by column"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Initially sorting should be disabled
assert not widget.isSortingEnabled()
# Enable sorting
widget.setSortingEnabled(True)
assert widget.isSortingEnabled()
# Test sorting by column
widget.sortByColumn(0, ttk.TTkK.SortOrder.AscendingOrder)
# Note: We can't easily verify the actual sorting without checking the model state
# Disable sorting
widget.setSortingEnabled(False)
assert not widget.isSortingEnabled()
def test_selection_methods(self):
"""Test selection-related methods"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Test clear selection
widget.clearSelection()
# Test select all
widget.selectAll()
# Test select row
widget.selectRow(0)
# Test select column
widget.selectColumn(1)
# Test set selection with position and size
widget.setSelection((0, 0), (2, 2), ttk.TTkK.TTkItemSelectionModel.Select)
# These methods should not raise exceptions
# Actual selection state testing would require more complex setup
def test_column_width_operations(self):
"""Test column width setting and resizing"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Test setting column width
widget.setColumnWidth(0, 20)
# Test resize column to contents
widget.resizeColumnToContents(0)
# Test resize all columns to contents
widget.resizeColumnsToContents()
# These methods should not raise exceptions
def test_row_height_operations(self):
"""Test row height setting and resizing"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Test setting row height
widget.setRowHeight(0, 3)
# Test resize row to contents
widget.resizeRowToContents(0)
# Test resize all rows to contents
widget.resizeRowsToContents()
# These methods should not raise exceptions
def test_clipboard_operations(self):
"""Test copy, cut, and paste operations"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# These operations should not raise exceptions
widget.copy()
widget.cut()
widget.paste()
def test_undo_redo_operations(self):
"""Test undo and redo operations"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Initially, there should be no undo/redo available
# But the methods should not raise exceptions
widget.undo()
widget.redo()
def test_signals_exist(self):
"""Test that all expected signals exist"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Test that signals are accessible
assert hasattr(widget, 'cellChanged')
assert hasattr(widget, 'cellClicked')
assert hasattr(widget, 'cellDoubleClicked')
assert hasattr(widget, 'cellEntered')
assert hasattr(widget, 'currentCellChanged')
# Test that they are signal objects
assert isinstance(widget.cellChanged, ttk.pyTTkSignal)
assert isinstance(widget.cellClicked, ttk.pyTTkSignal)
assert isinstance(widget.cellDoubleClicked, ttk.pyTTkSignal)
assert isinstance(widget.cellEntered, ttk.pyTTkSignal)
assert isinstance(widget.currentCellChanged, ttk.pyTTkSignal)
def test_signal_connections(self):
"""Test signal connections"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Create mock slots
cell_changed_slot = Mock()
cell_clicked_slot = Mock()
current_cell_changed_slot = Mock()
@ttk.pyTTkSlot(int,int)
def _mock_cell_changed_slot(row:int,col:int):
cell_changed_slot(row, col)
@ttk.pyTTkSlot(int,int)
def _mock_cell_clicked_slot(row:int,col:int):
cell_clicked_slot(row, col)
@ttk.pyTTkSlot(int,int,int,int)
def _mock_cell_cchanged_slot(a:int,b:int,c:int,d:int):
current_cell_changed_slot(a,b,c,d)
# Connect signals
widget.cellChanged.connect(_mock_cell_changed_slot)
widget.cellClicked.connect(_mock_cell_clicked_slot)
widget.currentCellChanged.connect(_mock_cell_cchanged_slot)
# Verify connections were made (signals should have connections)
assert len(widget.cellChanged._connected_slots) > 0
assert len(widget.cellClicked._connected_slots) > 0
assert len(widget.currentCellChanged._connected_slots) > 0
def test_model_change_propagation(self):
"""Test that model changes are propagated to the widget"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Modify the model
self.table_model.setData(0, 0, 'Modified')
# The widget should be notified (through signal connections)
# We can't easily test the actual update without a more complex setup
assert widget.model().data(0, 0) == 'Modified'
def test_empty_model_handling(self):
"""Test widget behavior with empty model"""
empty_model = ttk.TTkTableModelList(data=[[]])
widget = ttk.TTkTableWidget(tableModel=empty_model)
assert widget.rowCount() == 1
assert widget.columnCount() == 0
def test_large_model_handling(self):
"""Test widget behavior with large model"""
large_data = [[f'Cell_{i}_{j}' for j in range(100)] for i in range(100)]
large_model = ttk.TTkTableModelList(data=large_data)
widget = ttk.TTkTableWidget(tableModel=large_model)
assert widget.rowCount() == 100
assert widget.columnCount() == 100
def test_model_replacement(self):
"""Test replacing the model after widget creation"""
widget = ttk.TTkTableWidget()
original_row_count = widget.rowCount()
original_col_count = widget.columnCount()
# Replace with our test model
widget.setModel(self.table_model)
# Verify the change
assert widget.rowCount() != original_row_count
assert widget.columnCount() != original_col_count
assert widget.rowCount() == 3
assert widget.columnCount() == 3
def test_widget_with_different_model_types(self):
"""Test widget with different types of table models"""
# Test with TTkTableModelList
list_model = ttk.TTkTableModelList(data=self.test_data)
widget1 = ttk.TTkTableWidget(tableModel=list_model)
assert widget1.rowCount() == 3
# Test with TTkTableModelCSV (using in-memory data)
import io
csv_data = io.StringIO("Name,Age\nAlice,25\nBob,30")
csv_model = ttk.TTkTableModelCSV(fd=csv_data)
widget2 = ttk.TTkTableWidget(tableModel=csv_model)
assert widget2.rowCount() == 2
def test_header_visibility_changes(self):
"""Test changing header visibility after initialization"""
widget = ttk.TTkTableWidget()
# Initially headers should be visible
assert widget.verticalHeader().isVisible()
assert widget.horizontalHeader().isVisible()
# Hide headers
widget.verticalHeader().hide()
widget.horizontalHeader().hide()
assert not widget.verticalHeader().isVisible()
assert not widget.horizontalHeader().isVisible()
# Show headers again
widget.verticalHeader().show()
widget.horizontalHeader().show()
assert widget.verticalHeader().isVisible()
assert widget.horizontalHeader().isVisible()
def test_focus_policy(self):
"""Test that the widget has proper focus policy"""
widget = ttk.TTkTableWidget()
# Should accept click and tab focus
focus_policy = widget.focusPolicy()
assert focus_policy & ttk.TTkK.ClickFocus
assert focus_policy & ttk.TTkK.TabFocus
def test_minimum_size(self):
"""Test minimum size constraints"""
widget = ttk.TTkTableWidget()
# Should have minimum height of 1
min_size = widget.minimumSize()
assert min_size[1] >= 1 # height should be at least 1
def test_style_properties(self):
"""Test that the widget has proper style properties"""
widget = ttk.TTkTableWidget()
# Should have class style defined
assert hasattr(ttk.TTkTableWidget, 'classStyle')
assert isinstance(ttk.TTkTableWidget.classStyle, dict)
assert 'default' in ttk.TTkTableWidget.classStyle
def test_boundary_conditions(self):
"""Test boundary conditions and edge cases"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Test with invalid row/column indices (should not crash)
try:
widget.selectRow(-1)
widget.selectRow(1000)
widget.selectColumn(-1)
widget.selectColumn(1000)
widget.setColumnWidth(-1, 10)
widget.setColumnWidth(1000, 10)
widget.setRowHeight(-1, 2)
widget.setRowHeight(1000, 2)
except Exception as e:
# Some operations might raise exceptions, but they shouldn't crash
pass
def test_integration_with_scroll_area(self):
"""Test that the widget works as expected within a scroll area (via TTkTable)"""
table = ttk.TTkTable(tableModel=self.table_model)
# TTkTable should forward methods to the internal TTkTableWidget
assert table.rowCount() == 3
assert table.columnCount() == 3
assert table.model() == self.table_model
def test_zero_rows_model(self):
"""Test widget behavior with model that has zero rows"""
# Create model with zero rows but some columns
empty_rows_model = ttk.TTkTableModelList(
data=[],
header=['Col1', 'Col2', 'Col3']
)
widget = ttk.TTkTableWidget(tableModel=empty_rows_model)
assert widget.rowCount() == 0
assert widget.columnCount() == 0
assert widget.currentRow() == 0 # No valid current row
assert widget.currentColumn() == 0 # No valid current column
# Selection operations should handle empty rows gracefully
widget.clearSelection()
widget.selectAll() # Should not crash with no rows
# Column operations should still work
widget.setColumnWidth(0, 50)
widget.resizeColumnsToContents()
# Row operations should handle empty case
widget.resizeRowsToContents() # Should not crash with no rows
def test_zero_columns_model(self):
"""Test widget behavior with model that has zero columns"""
# Create model with zero columns but some rows
empty_cols_model = ttk.TTkTableModelList(
data=[[], [], []], # 3 empty rows
header=[]
)
widget = ttk.TTkTableWidget(tableModel=empty_cols_model)
assert widget.rowCount() == 3
assert widget.columnCount() == 0
assert widget.currentRow() == 0 # No valid current row
assert widget.currentColumn() == 0 # No valid current column
# Selection operations should handle empty columns gracefully
widget.clearSelection()
widget.selectAll() # Should not crash with no columns
# Row operations should still work
widget.setRowHeight(0, 2)
widget.resizeRowsToContents()
# Column operations should handle empty case
widget.resizeColumnsToContents() # Should not crash with no columns
def test_completely_empty_model(self):
"""Test widget behavior with model that has zero rows and zero columns"""
# Create completely empty model
empty_model = ttk.TTkTableModelList(
data=[],
header=[]
)
widget = ttk.TTkTableWidget(tableModel=empty_model)
assert widget.rowCount() == 0
assert widget.columnCount() == 0
assert widget.currentRow() == 0
assert widget.currentColumn() == 0
# All operations should handle completely empty case gracefully
widget.clearSelection()
widget.selectAll()
widget.resizeColumnsToContents()
widget.resizeRowsToContents()
# Clipboard operations with empty model
widget.copy() # Should not crash
widget.cut() # Should not crash
widget.paste() # Should not crash
# Undo/redo with empty model
widget.undo()
widget.redo()
def test_single_cell_model(self):
"""Test widget behavior with model that has exactly one cell"""
single_cell_model = ttk.TTkTableModelList(
data=[['SingleCell']],
header=['OnlyColumn']
)
widget = ttk.TTkTableWidget(tableModel=single_cell_model)
assert widget.rowCount() == 1
assert widget.columnCount() == 1
# Selection operations
widget.selectAll()
widget.selectRow(0)
widget.selectColumn(0)
widget.setSelection((0, 0), (1, 1), ttk.TTkK.TTkItemSelectionModel.Select)
# Resize operations
widget.setColumnWidth(0, 20)
widget.setRowHeight(0, 3)
widget.resizeColumnToContents(0)
widget.resizeRowToContents(0)
def test_model_transitions(self):
"""Test transitioning between different model sizes"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Start with normal model
assert widget.rowCount() == 3
assert widget.columnCount() == 3
# Transition to empty rows
empty_rows_model = ttk.TTkTableModelList(data=[], header=['A', 'B'])
widget.setModel(empty_rows_model)
assert widget.rowCount() == 0
assert widget.columnCount() == 0
# Transition to empty columns
empty_cols_model = ttk.TTkTableModelList(data=[[], []], header=[])
widget.setModel(empty_cols_model)
assert widget.rowCount() == 2
assert widget.columnCount() == 0
# Transition to completely empty
empty_model = ttk.TTkTableModelList(data=[], header=[])
widget.setModel(empty_model)
assert widget.rowCount() == 0
assert widget.columnCount() == 0
# Transition back to normal model
widget.setModel(self.table_model)
assert widget.rowCount() == 3
assert widget.columnCount() == 3
def test_boundary_operations_on_empty_models(self):
"""Test boundary operations on various empty model configurations"""
# Test with zero rows model
zero_rows_model = ttk.TTkTableModelList(data=[], header=['A', 'B'])
widget = ttk.TTkTableWidget(tableModel=zero_rows_model)
# These should not crash even with no rows
try:
widget.selectRow(0) # Invalid row
widget.unselectRow(0) # Invalid row
widget.setRowHeight(0, 2) # Invalid row
widget.resizeRowToContents(0) # Invalid row
except (IndexError, ValueError):
pass # Expected for invalid indices
# Test with zero columns model
zero_cols_model = ttk.TTkTableModelList(data=[[], []], header=[])
widget.setModel(zero_cols_model)
# These should not crash even with no columns
try:
widget.selectColumn(0) # Invalid column
widget.unselectColumn(0) # Invalid column
widget.setColumnWidth(0, 20) # Invalid column
widget.resizeColumnToContents(0) # Invalid column
except (IndexError, ValueError):
pass # Expected for invalid indices
def test_empty_model_sorting(self):
"""Test sorting operations on empty models"""
# Test sorting with zero rows
zero_rows_model = ttk.TTkTableModelList(data=[], header=['Name', 'Age'])
widget = ttk.TTkTableWidget(tableModel=zero_rows_model, sortingEnabled=True)
assert widget.isSortingEnabled()
# Sorting empty data should not crash
widget.sortByColumn(0, ttk.TTkK.SortOrder.AscendingOrder)
widget.sortByColumn(1, ttk.TTkK.SortOrder.DescendingOrder)
# Test sorting with zero columns
zero_cols_model = ttk.TTkTableModelList(data=[[], []], header=[])
widget.setModel(zero_cols_model)
# Sorting with no columns should handle gracefully
try:
widget.sortByColumn(0, ttk.TTkK.SortOrder.AscendingOrder) # Invalid column
except (IndexError, ValueError):
pass # Expected for invalid column index
def test_empty_model_signals(self):
"""Test that signals work correctly with empty models"""
empty_model = ttk.TTkTableModelList(data=[], header=[])
widget = ttk.TTkTableWidget(tableModel=empty_model)
# Connect signals to mock slots
cell_changed_calls = []
cell_clicked_calls = []
@ttk.pyTTkSlot(int, int)
def mock_cell_changed(row, col):
cell_changed_calls.append((row, col))
@ttk.pyTTkSlot(int, int)
def mock_cell_clicked(row, col):
cell_clicked_calls.append((row, col))
widget.cellChanged.connect(mock_cell_changed)
widget.cellClicked.connect(mock_cell_clicked)
# Signals should be connected even with empty model
assert len(widget.cellChanged._connected_slots) > 0
assert len(widget.cellClicked._connected_slots) > 0
# No signals should be emitted for empty model operations
widget.clearSelection()
widget.selectAll()
# Should have no signal calls since there are no cells
assert len(cell_changed_calls) == 0
assert len(cell_clicked_calls) == 0
def test_integration_with_full_application():
"""Integration test - verify TTkTableWidget works in a full application context"""
# Create a more complex scenario
data = [
['Product A', 100, 29.99],
['Product B', 50, 19.99],
['Product C', 75, 39.99]
]
headers = ['Product', 'Stock', 'Price']
model = ttk.TTkTableModelList(data=data, header=headers)
widget = ttk.TTkTableWidget(
tableModel=model,
sortingEnabled=True,
vSeparator=True,
hSeparator=True
)
# Test various operations in sequence
widget.resizeColumnsToContents()
widget.setSortingEnabled(True)
widget.sortByColumn(2, ttk.TTkK.SortOrder.DescendingOrder) # Sort by price descending
widget.selectRow(0)
widget.copy()
# Verify the widget still functions correctly
assert widget.rowCount() == 3
assert widget.columnCount() == 3
assert widget.isSortingEnabled()
if __name__ == '__main__':
pytest.main([__file__])

4
tests/t.ui/test.ui.032.table.12.py

@ -397,7 +397,7 @@ def _insertRows():
@ttk.pyTTkSlot()
def _deleteRows():
_model = table.model()
_model.removeRows(5,5)
_model.removeRows(0,5)
@ttk.pyTTkSlot()
def _insertCols():
@ -407,7 +407,7 @@ def _insertCols():
@ttk.pyTTkSlot()
def _deleteCols():
_model = table.model()
_model.removeColumns(5,5)
_model.removeColumns(0,5)
btn_ins_row.clicked.connect(_insertRows)
btn_del_row.clicked.connect(_deleteRows)

25
tests/t.ui/test.ui.032.table.13.alignment.02.overloading.py

@ -47,7 +47,7 @@ args = parser.parse_args()
fullScreen = not args.w
mouseTrack = True
class MyTableMixin():
class MyTableModel(ttk.TTkTableModelList):
def headerData(self, num, orientation):
if orientation == ttk.TTkK.HORIZONTAL:
if 0 == num%4:
@ -61,28 +61,15 @@ class MyTableMixin():
return super().headerData(num, orientation)
def displayData(self:ttk.TTkTableModelList, row:int, col:int) -> Tuple[ttk.TTkString, ttk.TTkK.Alignment]:
data, legacy_align = super().displayData(row,col)
if 0 == col%4:
return data, ttk.TTkK.Alignment.LEFT_ALIGN
return self.ttkStringData(row, col), ttk.TTkK.Alignment.LEFT_ALIGN
if 1 == col%4:
return data, ttk.TTkK.Alignment.CENTER_ALIGN
return self.ttkStringData(row, col), ttk.TTkK.Alignment.CENTER_ALIGN
if 2 == col%4:
return data, ttk.TTkK.Alignment.RIGHT_ALIGN
return self.ttkStringData(row, col), ttk.TTkK.Alignment.RIGHT_ALIGN
if 3 == col%4:
return data, ttk.TTkK.Alignment.JUSTIFY
return data, legacy_align
class MyTableModel(MyTableMixin, ttk.TTkTableModelList):
def flags(self, row: int, col: int) -> ttk.TTkConstant.ItemFlag:
if col==0:
return (
ttk.TTkK.ItemFlag.ItemIsEnabled |
ttk.TTkK.ItemFlag.ItemIsSelectable )
if col==1:
return (
ttk.TTkK.ItemFlag.ItemIsEnabled |
ttk.TTkK.ItemFlag.ItemIsEditable )
return super().flags(row, col)
return self.ttkStringData(row, col), ttk.TTkK.Alignment.JUSTIFY
return super().displayData(row,col)
data_list1 = [[f"{y:03}\npippo\npeppo-ooo"]+[str(x) for x in range(10) ] for y in range(20)]
data_list1[1][1] = "abc def ghi\ndef ghi\nghi\njkl - pippo"

25
tests/t.ui/test.ui.032.table.13.alignment.03.mixin.py

@ -47,7 +47,7 @@ args = parser.parse_args()
fullScreen = not args.w
mouseTrack = True
class MyTableModel(ttk.TTkTableModelList):
class MyTableMixin():
def headerData(self, num, orientation):
if orientation == ttk.TTkK.HORIZONTAL:
if 0 == num%4:
@ -61,15 +61,28 @@ class MyTableModel(ttk.TTkTableModelList):
return super().headerData(num, orientation)
def displayData(self:ttk.TTkTableModelList, row:int, col:int) -> Tuple[ttk.TTkString, ttk.TTkK.Alignment]:
data, legacy_align = super().displayData(row,col)
if 0 == col%4:
return self.ttkStringData(row, col), ttk.TTkK.Alignment.LEFT_ALIGN
return data, ttk.TTkK.Alignment.LEFT_ALIGN
if 1 == col%4:
return self.ttkStringData(row, col), ttk.TTkK.Alignment.CENTER_ALIGN
return data, ttk.TTkK.Alignment.CENTER_ALIGN
if 2 == col%4:
return self.ttkStringData(row, col), ttk.TTkK.Alignment.RIGHT_ALIGN
return data, ttk.TTkK.Alignment.RIGHT_ALIGN
if 3 == col%4:
return self.ttkStringData(row, col), ttk.TTkK.Alignment.JUSTIFY
return super().displayData(row,col)
return data, ttk.TTkK.Alignment.JUSTIFY
return data, legacy_align
class MyTableModel(MyTableMixin, ttk.TTkTableModelList):
def flags(self, row: int, col: int) -> ttk.TTkConstant.ItemFlag:
if col==0:
return (
ttk.TTkK.ItemFlag.ItemIsEnabled |
ttk.TTkK.ItemFlag.ItemIsSelectable )
if col==1:
return (
ttk.TTkK.ItemFlag.ItemIsEnabled |
ttk.TTkK.ItemFlag.ItemIsEditable )
return super().flags(row, col)
data_list1 = [[f"{y:03}\npippo\npeppo-ooo"]+[str(x) for x in range(10) ] for y in range(20)]
data_list1[1][1] = "abc def ghi\ndef ghi\nghi\njkl - pippo"

Loading…
Cancel
Save