tksheet 7.5.4__py3-none-any.whl → 7.5.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tksheet/functions.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import copy
3
4
  import csv
4
5
  import io
5
6
  import re
@@ -9,6 +10,7 @@ from collections import deque
9
10
  from collections.abc import Callable, Generator, Hashable, Iterable, Iterator, Sequence
10
11
  from difflib import SequenceMatcher
11
12
  from itertools import chain, islice, repeat
13
+ from types import ModuleType
12
14
  from typing import Any, Literal
13
15
 
14
16
  from .colors import color_map
@@ -16,7 +18,6 @@ from .constants import align_value_error, symbols_set
16
18
  from .formatters import to_bool
17
19
  from .other_classes import DotDict, EventDataDict, Highlight, Loc, Span
18
20
 
19
- lines_re = re.compile(r"[^\n]+")
20
21
  ORD_A = ord("A")
21
22
 
22
23
 
@@ -33,8 +34,8 @@ def wrap_text(
33
34
  line_width = 0
34
35
  if wrap == "c":
35
36
  current_line = []
36
- for match in lines_re.finditer(text):
37
- for char in match.group():
37
+ for line in text.split("\n"):
38
+ for char in line:
38
39
  try:
39
40
  char_width = widths[char]
40
41
  except KeyError:
@@ -75,8 +76,8 @@ def wrap_text(
75
76
  space_width = char_width_fn(" ")
76
77
  current_line = []
77
78
 
78
- for match in lines_re.finditer(text):
79
- for i, word in enumerate(match.group().split()):
79
+ for line in text.split("\n"):
80
+ for i, word in enumerate(line.split()):
80
81
  # if we're going to next word and
81
82
  # if a space fits on the end of the current line we add one
82
83
  if i and line_width + space_width < max_width:
@@ -155,10 +156,10 @@ def wrap_text(
155
156
  line_width = 0
156
157
 
157
158
  else:
158
- for match in lines_re.finditer(text):
159
+ for line in text.split("\n"):
159
160
  line_width = 0
160
161
  current_line = []
161
- for char in match.group():
162
+ for char in line:
162
163
  try:
163
164
  char_width = widths[char]
164
165
  except KeyError:
@@ -205,6 +206,16 @@ def get_data_from_clipboard(
205
206
  return [[data]]
206
207
 
207
208
 
209
+ def widget_descendants(widget: tk.Misc) -> list[tk.Misc]:
210
+ result = []
211
+ queue = deque([widget])
212
+ while queue:
213
+ current_widget = queue.popleft()
214
+ result.append(current_widget)
215
+ queue.extend(current_widget.winfo_children())
216
+ return result
217
+
218
+
208
219
  def recursive_bind(widget: tk.Misc, event: str, callback: Callable) -> None:
209
220
  widget.bind(event, callback)
210
221
  for child in widget.winfo_children():
@@ -461,6 +472,8 @@ def get_dropdown_kwargs(
461
472
  search_function: Callable = dropdown_search_function,
462
473
  validate_input: bool = True,
463
474
  text: None | str = None,
475
+ edit_data: bool = True,
476
+ default_value: Any = None,
464
477
  ) -> dict:
465
478
  return {
466
479
  "values": [] if values is None else values,
@@ -472,6 +485,8 @@ def get_dropdown_kwargs(
472
485
  "search_function": search_function,
473
486
  "validate_input": validate_input,
474
487
  "text": text,
488
+ "edit_data": edit_data,
489
+ "default_value": default_value,
475
490
  }
476
491
 
477
492
 
@@ -484,6 +499,7 @@ def get_dropdown_dict(**kwargs) -> dict:
484
499
  "validate_input": kwargs["validate_input"],
485
500
  "text": kwargs["text"],
486
501
  "state": kwargs["state"],
502
+ "default_value": kwargs["default_value"],
487
503
  }
488
504
 
489
505
 
@@ -765,6 +781,14 @@ def add_to_displayed(displayed: list[int], to_add: Iterable[int]) -> list[int]:
765
781
  return displayed
766
782
 
767
783
 
784
+ def push_displayed(displayed: list[int], to_add: Iterable[int]) -> list[int]:
785
+ # assumes to_add is sorted
786
+ for i in to_add:
787
+ ins = bisect_left(displayed, i)
788
+ displayed[ins:] = [e + 1 for e in islice(displayed, ins, None)]
789
+ return displayed
790
+
791
+
768
792
  def move_elements_by_mapping(
769
793
  seq: list[Any],
770
794
  new_idxs: dict[int, int],
@@ -873,50 +897,38 @@ def rounded_box_coords(
873
897
  x2: float,
874
898
  y2: float,
875
899
  radius: int = 5,
876
- ) -> tuple[float]:
900
+ ) -> tuple[float, ...]:
901
+ # Handle case where rectangle is too small for rounding
877
902
  if y2 - y1 < 2 or x2 - x1 < 2:
878
- return x1, y1, x2, y1, x2, y2, x1, y2
903
+ return (x1, y1, x2, y1, x2, y2, x1, y2, x1, y1)
904
+ # Coordinates for a closed rectangle with rounded corners
879
905
  return (
880
906
  x1 + radius,
881
- y1,
882
- x1 + radius,
883
- y1,
907
+ y1, # Top side start
884
908
  x2 - radius,
885
- y1,
886
- x2 - radius,
887
- y1,
909
+ y1, # Top side end
888
910
  x2,
889
- y1,
911
+ y1, # Top-right corner
890
912
  x2,
891
913
  y1 + radius,
892
914
  x2,
893
- y1 + radius,
894
- x2,
895
- y2 - radius,
896
- x2,
897
- y2 - radius,
915
+ y2 - radius, # Right side
898
916
  x2,
899
- y2,
900
- x2 - radius,
901
- y2,
917
+ y2, # Bottom-right corner
902
918
  x2 - radius,
903
919
  y2,
904
920
  x1 + radius,
905
- y2,
906
- x1 + radius,
907
- y2,
921
+ y2, # Bottom side
908
922
  x1,
909
- y2,
910
- x1,
911
- y2 - radius,
923
+ y2, # Bottom-left corner
912
924
  x1,
913
925
  y2 - radius,
914
926
  x1,
915
- y1 + radius,
927
+ y1 + radius, # Left side
916
928
  x1,
917
- y1 + radius,
918
- x1,
919
- y1,
929
+ y1, # Top-left corner
930
+ x1 + radius,
931
+ y1, # Close the shape
920
932
  )
921
933
 
922
934
 
@@ -1195,14 +1207,14 @@ PATTERN_ALL = re.compile(r"^:$") # ":"
1195
1207
 
1196
1208
  def span_a2i(a: str) -> int | None:
1197
1209
  n = 0
1198
- for c in a:
1210
+ for c in a.upper():
1199
1211
  n = n * 26 + ord(c) - ORD_A + 1
1200
1212
  return n - 1
1201
1213
 
1202
1214
 
1203
1215
  def span_a2n(a: str) -> int | None:
1204
1216
  n = 0
1205
- for c in a:
1217
+ for c in a.upper():
1206
1218
  n = n * 26 + ord(c) - ORD_A + 1
1207
1219
  return n
1208
1220
 
@@ -1239,7 +1251,7 @@ def key_to_span(
1239
1251
  return coords_to_span(widget=widget, from_r=None, from_c=None, upto_r=None, upto_c=None)
1240
1252
 
1241
1253
  # Validate input type
1242
- elif not isinstance(key, (str, int, slice, list, tuple)):
1254
+ elif not isinstance(key, (str, int, slice, tuple, list)):
1243
1255
  return f"Key type must be either str, int, list, tuple or slice, not '{type(key).__name__}'."
1244
1256
 
1245
1257
  try:
@@ -1265,22 +1277,34 @@ def key_to_span(
1265
1277
  )
1266
1278
 
1267
1279
  # Sequence key: various span formats
1268
- elif isinstance(key, (list, tuple)):
1269
- if (
1270
- len(key) == 2
1271
- and (isinstance(key[0], int) or key[0] is None)
1272
- and (isinstance(key[1], int) or key[1] is None)
1273
- ):
1274
- # Single cell or partial span: (row, col)
1275
- r_int = isinstance(key[0], int)
1276
- c_int = isinstance(key[1], int)
1277
- return span_dict(
1278
- from_r=key[0] if r_int else 0,
1279
- from_c=key[1] if c_int else 0,
1280
- upto_r=key[0] + 1 if r_int else None,
1281
- upto_c=key[1] + 1 if c_int else None,
1282
- widget=widget,
1283
- )
1280
+ elif isinstance(key, (tuple, list)):
1281
+ if len(key) == 2:
1282
+ if (isinstance(key[0], int) or key[0] is None) and (isinstance(key[1], int) or key[1] is None):
1283
+ # Single cell or partial span: (row, col)
1284
+ r_int = isinstance(key[0], int)
1285
+ c_int = isinstance(key[1], int)
1286
+ return span_dict(
1287
+ from_r=key[0] if r_int else 0,
1288
+ from_c=key[1] if c_int else 0,
1289
+ upto_r=key[0] + 1 if r_int else None,
1290
+ upto_c=key[1] + 1 if c_int else None,
1291
+ widget=widget,
1292
+ )
1293
+
1294
+ elif isinstance(key[0], int) and isinstance(key[1], str):
1295
+ # Single cell with column letter: (row 0, col A)
1296
+ c_int = span_a2i(key[1])
1297
+ return span_dict(
1298
+ from_r=key[0],
1299
+ from_c=c_int,
1300
+ upto_r=key[0] + 1,
1301
+ upto_c=c_int + 1,
1302
+ widget=widget,
1303
+ )
1304
+
1305
+ else:
1306
+ return f"'{key}' could not be converted to span."
1307
+
1284
1308
  elif len(key) == 4:
1285
1309
  # Full span coordinates: (from_r, from_c, upto_r, upto_c)
1286
1310
  return coords_to_span(
@@ -1290,7 +1314,7 @@ def key_to_span(
1290
1314
  upto_r=key[2],
1291
1315
  upto_c=key[3],
1292
1316
  )
1293
- elif len(key) == 2 and all(isinstance(k, (list, tuple)) for k in key):
1317
+ elif len(key) == 2 and all(isinstance(k, (tuple, list)) for k in key):
1294
1318
  # Start and end points: ((from_r, from_c), (upto_r, upto_c))
1295
1319
  return coords_to_span(
1296
1320
  widget=widget,
@@ -1327,11 +1351,12 @@ def key_to_span(
1327
1351
  widget=widget,
1328
1352
  )
1329
1353
  elif m := PATTERN_COL.match(key):
1354
+ c_int = span_a2i(m[1])
1330
1355
  return span_dict(
1331
1356
  from_r=None,
1332
- from_c=span_a2i(m[1]),
1357
+ from_c=c_int,
1333
1358
  upto_r=None,
1334
- upto_c=span_a2n(m[1]),
1359
+ upto_c=c_int + 1,
1335
1360
  widget=widget,
1336
1361
  )
1337
1362
  elif m := PATTERN_CELL.match(key):
@@ -1499,6 +1524,17 @@ def del_named_span_options_nested(
1499
1524
  del options[k][type_]
1500
1525
 
1501
1526
 
1527
+ def mod_note(options: dict, key: int | tuple[int, int], note: str | None, readonly: bool = True) -> dict:
1528
+ if note is not None:
1529
+ if key not in options:
1530
+ options[key] = {}
1531
+ options[key]["note"] = {"note": note, "readonly": readonly}
1532
+ else:
1533
+ if key in options and "note" in options[key]:
1534
+ del options[key]["note"]
1535
+ return options
1536
+
1537
+
1502
1538
  def add_highlight(
1503
1539
  options: dict,
1504
1540
  key: int | tuple[int, int],
@@ -1733,7 +1769,7 @@ def get_vertical_gridline_points(
1733
1769
  positions: list[float],
1734
1770
  start: int,
1735
1771
  end: int,
1736
- ) -> list[float]:
1772
+ ) -> list[int | float]:
1737
1773
  return list(
1738
1774
  chain.from_iterable(
1739
1775
  (
@@ -1749,3 +1785,44 @@ def get_vertical_gridline_points(
1749
1785
  for c in range(start, end)
1750
1786
  )
1751
1787
  )
1788
+
1789
+
1790
+ def safe_copy(value: Any) -> Any:
1791
+ """
1792
+ Attempts to create a deep copy of the input value. If copying fails,
1793
+ returns the original value.
1794
+
1795
+ Args:
1796
+ value: Any Python object to be copied
1797
+
1798
+ Returns:
1799
+ A deep copy of the value if possible, otherwise the original value
1800
+ """
1801
+ try:
1802
+ # Try deep copy first for most objects
1803
+ return copy.deepcopy(value)
1804
+ except Exception:
1805
+ try:
1806
+ # For types that deepcopy might fail on, try shallow copy
1807
+ return copy.copy(value)
1808
+ except Exception:
1809
+ try:
1810
+ # For built-in immutable types, return as-is
1811
+ if isinstance(value, (int, float, str, bool, bytes, tuple, frozenset)):
1812
+ return value
1813
+ # For None
1814
+ if value is None:
1815
+ return None
1816
+ # For functions, return same function
1817
+ if isinstance(value, Callable):
1818
+ return value
1819
+ # For modules
1820
+ if isinstance(value, ModuleType):
1821
+ return value
1822
+ # For classes
1823
+ if isinstance(value, type):
1824
+ return value
1825
+ except Exception:
1826
+ pass
1827
+ # If all copy attempts fail, return original value
1828
+ return value