Skip to content

Commit

Permalink
7.4.1 bug fixes
Browse files Browse the repository at this point in the history
#### Fixed:
- Issues with `7.4.0` sorting
- [#270](#270)
- Only add to index header when user adds rows/columns if index/header is populated
  • Loading branch information
ragardner committed Feb 19, 2025
1 parent f094570 commit 60d43b7
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 132 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@
</tbody>
</table>

This library is maintained with the help of **[others](https://github.com/ragardner/tksheet/graphs/contributors)**. If you would like to contribute please read this [help section](https://github.com/ragardner/tksheet/wiki/Version-7#contributing).

## **Features**

- Smoothly display and modify tabular data
Expand Down
5 changes: 4 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
### Version 7.4.1
####
#### Fixed:
- Issues with `7.4.0` sorting
- [#270](https://github.com/ragardner/tksheet/issues/270)
- Only add to index header when user adds rows/columns if index/header is populated

### Version 7.4.0
#### Changed:
Expand Down
11 changes: 5 additions & 6 deletions docs/DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6282,15 +6282,14 @@ redraw(redraw_header: bool = True, redraw_row_index: bool = True) -> Sheet
---
# **Treeview Mode**

tksheet has a treeview mode which behaves similarly to the ttk treeview widget, it is not a drop in replacement for it though. All functionality should work as with the non-treeview mode in tksheet versions >= `7.4.0`.
tksheet has a treeview mode which behaves similarly to the ttk treeview widget, it is not a drop in replacement for it though.

Always either use a fresh `Sheet()` instance or use [Sheet.reset()](https://github.com/ragardner/tksheet/wiki/Version-7#reset-all-or-specific-sheet-elements-and-attributes) before enabling treeview mode.

### **Treeview limitations**
**Text alignment**
The index text alignment must be `"w"` aka west or left.
### **TO NOTE:**
- When treeview mode is enabled the row index is a `list` of `Node` objects. The row index should not be modified by the usual `row_index()` function.
- Most other tksheet functions should work as normal.
- The index text alignment must be `"w"` aka west or left.

## **Creating a treeview mode sheet**

Expand Down
68 changes: 40 additions & 28 deletions tksheet/main_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,31 +275,21 @@ def __init__(
self.all_columns_displayed = True
self.all_rows_displayed = True
self.align = kwargs["align"]

self.PAR.ops.table_font = [
self.PAR.ops.table_font = FontTuple(
self.PAR.ops.table_font[0],
int(self.PAR.ops.table_font[1] * kwargs["zoom"] / 100),
max(1, int(self.PAR.ops.table_font[1] * kwargs["zoom"] / 100)),
self.PAR.ops.table_font[2],
]

self.PAR.ops.index_font = [
)
self.PAR.ops.index_font = FontTuple(
self.PAR.ops.index_font[0],
int(self.PAR.ops.index_font[1] * kwargs["zoom"] / 100),
max(1, int(self.PAR.ops.index_font[1] * kwargs["zoom"] / 100)),
self.PAR.ops.index_font[2],
]

self.PAR.ops.header_font = [
)
self.PAR.ops.header_font = FontTuple(
self.PAR.ops.header_font[0],
int(self.PAR.ops.header_font[1] * kwargs["zoom"] / 100),
max(1, int(self.PAR.ops.header_font[1] * kwargs["zoom"] / 100)),
self.PAR.ops.header_font[2],
]
for fnt in (self.PAR.ops.table_font, self.PAR.ops.index_font, self.PAR.ops.header_font):
if fnt[1] < 1:
fnt[1] = 1
self.PAR.ops.table_font = FontTuple(*self.PAR.ops.table_font)
self.PAR.ops.index_font = FontTuple(*self.PAR.ops.index_font)
self.PAR.ops.header_font = FontTuple(*self.PAR.ops.header_font)

)
self.txt_measure_canvas = tk.Canvas(self)
self.txt_measure_canvas_text = self.txt_measure_canvas.create_text(0, 0, text="", font=self.PAR.ops.table_font)

Expand Down Expand Up @@ -1155,7 +1145,7 @@ def ctrl_v(self, event: object = None, validation: bool = True) -> None | EventD
if ctr:
event_data = self.add_rows(
rows=rows,
index=index,
index=index if isinstance(self._row_index, list) and self._row_index else {},
row_heights=row_heights,
event_data=event_data,
mod_event_boxes=False,
Expand Down Expand Up @@ -1205,7 +1195,7 @@ def ctrl_v(self, event: object = None, validation: bool = True) -> None | EventD
if ctr:
event_data = self.add_columns(
columns=columns,
header=headers,
header=headers if isinstance(self._headers, list) and self._headers else {},
column_widths=column_widths,
event_data=event_data,
mod_event_boxes=False,
Expand Down Expand Up @@ -5009,8 +4999,11 @@ def rc_add_columns(self, event: object = None):
event_data = self.new_event_dict("add_columns", state=True)
if not try_binding(self.extra_begin_insert_cols_rc_func, event_data, "begin_add_columns"):
return
columns, headers, widths = self.get_args_for_add_columns(data_ins_col, displayed_ins_col, numcols)
event_data = self.add_columns(
*self.get_args_for_add_columns(data_ins_col, displayed_ins_col, numcols),
columns=columns,
header=headers if isinstance(self._headers, list) and self._headers else {},
column_widths=widths,
event_data=event_data,
)
if self.undo_enabled:
Expand Down Expand Up @@ -5138,8 +5131,11 @@ def rc_add_rows(self, event: object = None):
event_data = self.new_event_dict("add_rows", state=True)
if not try_binding(self.extra_begin_insert_rows_rc_func, event_data, "begin_add_rows"):
return
rows, index, heights = self.get_args_for_add_rows(data_ins_row, displayed_ins_row, numrows)
event_data = self.add_rows(
*self.get_args_for_add_rows(data_ins_row, displayed_ins_row, numrows),
rows=rows,
index=index if isinstance(self._row_index, list) and self._row_index else {},
row_heights=heights,
event_data=event_data,
)
if self.undo_enabled:
Expand Down Expand Up @@ -5346,12 +5342,22 @@ def delete_columns(
return event_data
if not ext and not try_binding(self.extra_begin_del_cols_rc_func, event_data, "begin_delete_columns"):
return
if self.all_columns_displayed:
data_columns = columns
disp_columns = columns
else:
if data_indexes:
data_columns = columns
disp_columns = data_to_displayed_idxs(data_columns, self.displayed_columns)
else:
data_columns = [self.displayed_columns[c] for c in columns]
disp_columns = columns
event_data = self.delete_columns_displayed(
data_to_displayed_idxs(columns, self.MT.displayed_columns) if data_indexes else columns,
disp_columns,
event_data,
)
event_data = self.delete_columns_data(
columns if data_indexes or self.all_columns_displayed else [self.displayed_columns[c] for c in columns],
data_columns,
event_data,
)
if undo and self.undo_enabled:
Expand Down Expand Up @@ -5425,10 +5431,16 @@ def delete_rows(
return
if not ext and not try_binding(self.extra_begin_del_rows_rc_func, event_data, "begin_delete_rows"):
return
if data_indexes or self.all_rows_displayed:
if self.all_rows_displayed:
data_rows = rows
disp_rows = rows
else:
data_rows = [self.displayed_rows[r] for r in rows]
if data_indexes:
data_rows = rows
disp_rows = data_to_displayed_idxs(data_rows, self.displayed_rows)
else:
data_rows = [self.displayed_rows[r] for r in rows]
disp_rows = rows
if self.PAR.ops.treeview:
data_rows = sorted(
chain(
Expand All @@ -5441,7 +5453,7 @@ def delete_rows(
)
)
event_data = self.delete_rows_displayed(
data_to_displayed_idxs(data_rows, self.displayed_rows) if self.PAR.ops.treeview or data_indexes else rows,
disp_rows,
event_data,
)
event_data = self.delete_rows_data(
Expand Down
108 changes: 13 additions & 95 deletions tksheet/sorting.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from __future__ import annotations

import re
import unittest
from collections.abc import Callable, Generator
from datetime import datetime
from re import finditer

from .other_classes import Node
from .tksheet_types import AnyIter
Expand Down Expand Up @@ -59,48 +58,49 @@ def natural_sort_key(item: object) -> tuple[int, object]:
- Strings with natural sorting for embedded numbers and dates
- Unknown types treated as strings or left at the end
With love from Grok ❤️
Args:
item: Any Python object to be sorted.
Returns:
A tuple or value that can be used for sorting.
"""
if item is None:
return (0, "")
return (0,)

elif isinstance(item, bool):
return (1, item)

elif isinstance(item, (int, float)):
return (2, (item,)) # Tuple to ensure float and int are sorted together
return (2, item)

elif isinstance(item, datetime):
return (3, item.timestamp())

elif isinstance(item, str):
# Check if the whole string is a date
for date_format in date_formats:
try:
# Use the same sort order as for datetime objects
return (3, datetime.strptime(item, date_format).timestamp())
except ValueError:
continue

# Check if the whole string is a number
try:
return (4, float(item))
return (2, float(item))
except Exception:
# Proceed with natural sorting
return (5, tuple(int(text) if text.isdigit() else text.lower() for text in re.split(r"(\d+)", item)))
n = []
s = []
for match in finditer(r"\d+|[^\d\s]+", item):
if (m := match.group()).isdigit():
n.append(int(m))
else:
s.append(m.lower())
return (5, s, n)

else:
# For unknown types, attempt to convert to string, or place at end
try:
return (6, f"{item}".lower())
except Exception:
return (7, item) # If conversion fails, place at the very end
return (7, item)


def sort_selection(
Expand Down Expand Up @@ -285,85 +285,3 @@ def sort_tree_view(
new_index += 1

return sorted_nodes, mapping


class TestNaturalSort(unittest.TestCase):
def test_none_first(self):
self.assertEqual(natural_sort_key(None), (0, ""))

def test_booleans_order(self):
self.assertLess(natural_sort_key(False), natural_sort_key(True))

def test_numbers_order(self):
self.assertLess(natural_sort_key(5), natural_sort_key(10))
self.assertLess(natural_sort_key(5.5), natural_sort_key(6))

def test_datetime_order(self):
dt1 = datetime(2023, 1, 1)
dt2 = datetime(2023, 1, 2)
self.assertLess(natural_sort_key(dt1), natural_sort_key(dt2))

def test_string_natural_sort(self):
items = ["item2", "item10", "item1"]
sorted_items = sorted(items, key=natural_sort_key)
self.assertEqual(sorted_items, ["item1", "item2", "item10"])

def test_date_string_recognition(self):
# Test various date formats
date_str1 = "01/01/2023"
date_str2 = "2023-01-01"
date_str3 = "Jan 1, 2023"

dt = datetime(2023, 1, 1)

self.assertEqual(natural_sort_key(date_str1)[0], 3)
self.assertEqual(natural_sort_key(date_str2)[0], 3)
self.assertEqual(natural_sort_key(date_str3)[0], 3)
self.assertEqual(natural_sort_key(date_str1)[1], natural_sort_key(dt)[1]) # Timestamps should match

def test_unknown_types(self):
# Here we use a custom class for testing unknown types
class Unknown:
pass

unknown = Unknown()
self.assertEqual(natural_sort_key(unknown)[0], 5) # Success case, string conversion works

def test_unknown_types_failure(self):
# Create an object where string conversion fails
class Unconvertible:
def __str__(self):
raise Exception("String conversion fails")

def __repr__(self):
raise Exception("String conversion fails")

unconvertible = Unconvertible()
self.assertEqual(natural_sort_key(unconvertible)[0], 6) # Failure case, string conversion fails


def test_sort_selection():
# Test case 1: Mixed types, no reverse
data1 = [[1, "b"], [3, "a"]]
sorted_data1 = sort_selection(data1)
print(f"Test 1 - No reverse: {data1} -> {sorted_data1}")

# Test case 2: Mixed types, with reverse
data2 = [[1, "b"], [3, "a"]]
sorted_data2 = sort_selection(data2, reverse=True)
print(f"Test 2 - With reverse: {data2} -> {sorted_data2}")

# Test case 3: All numbers
data3 = [[2, 1], [4, 3]]
sorted_data3 = sort_selection(data3)
print(f"Test 3 - All numbers: {data3} -> {sorted_data3}")

# Test case 4: With None values
data4 = [[None, "b"], ["a", None]]
sorted_data4 = sort_selection(data4)
print(f"Test 4 - With None: {data4} -> {sorted_data4}")


if __name__ == "__main__":
test_sort_selection()
unittest.main()

0 comments on commit 60d43b7

Please sign in to comment.