pgwidgets-python 0.3.4__py3-none-any.whl → 0.4.0__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.
pgwidgets/sync/widget.py CHANGED
@@ -11,6 +11,7 @@ import base64
11
11
  import mimetypes
12
12
  import os
13
13
 
14
+ from pgwidgets import tree_model
14
15
  from pgwidgets.defs import WIDGETS, CALLBACK_METHODS, WIDGET_METHODS, CONTAINER_METHODS
15
16
  from pgwidgets.method_types import (
16
17
  classify_method, SETTER, GETTER, CHILD, ACTION, JS_ONLY,
@@ -248,6 +249,18 @@ class Widget:
248
249
  """The Session this widget belongs to."""
249
250
  return self._session
250
251
 
252
+ def batch(self):
253
+ """Convenience for ``widget.session.batch()``.
254
+
255
+ Batching is per session, not per widget, so updates to other
256
+ widgets made inside the block ride along in the same message::
257
+
258
+ with tree.batch():
259
+ tree.set_cell(path, 'seeing', '1.4')
260
+ status.set_text('updated')
261
+ """
262
+ return self._session.batch()
263
+
251
264
  @property
252
265
  def app(self):
253
266
  """The Application this widget belongs to."""
@@ -771,7 +784,8 @@ def _add_tree_view_methods(attrs, all_methods):
771
784
  if path is not None:
772
785
  key = tuple(path) if isinstance(path, list) else path
773
786
  expanded = self._state.setdefault("_expanded_paths", set())
774
- expanded.add(key)
787
+ if expanded != "_all":
788
+ expanded.add(key)
775
789
  collapsed = self._state.get("_collapsed_paths")
776
790
  if collapsed is not None and collapsed != "_all":
777
791
  collapsed.discard(key)
@@ -787,7 +801,7 @@ def _add_tree_view_methods(attrs, all_methods):
787
801
  if path is not None:
788
802
  key = tuple(path) if isinstance(path, list) else path
789
803
  expanded = self._state.get("_expanded_paths")
790
- if expanded is not None:
804
+ if expanded is not None and expanded != "_all":
791
805
  expanded.discard(key)
792
806
  collapsed = self._state.setdefault("_collapsed_paths", set())
793
807
  if collapsed != "_all":
@@ -812,6 +826,427 @@ def _add_tree_view_methods(attrs, all_methods):
812
826
  collapse_all_method.__name__ = "collapse_all"
813
827
  attrs["collapse_all"] = collapse_all_method
814
828
 
829
+ _add_tree_model_methods(attrs, all_methods)
830
+
831
+
832
+ def _pad(args, n):
833
+ """Pad a call's positional args out to `n` with None, so a partially
834
+ specified call still lands in the right state slots."""
835
+ args = list(args)
836
+ while len(args) < n:
837
+ args.append(None)
838
+ return tuple(args)
839
+
840
+
841
+ def _style_path(path):
842
+ """Hashable form of a path, for use as a style-map key."""
843
+ return tuple(path) if isinstance(path, list) else path
844
+
845
+
846
+ def _model_rows(widget):
847
+ """The flat row model in _state, whichever bulk setter filled it."""
848
+ for key in ("rows", "data"):
849
+ rows = widget._state.get(key)
850
+ if isinstance(rows, list):
851
+ return rows
852
+ return None
853
+
854
+
855
+ def _drop_row_styles(widget):
856
+ """The JS clear() that precedes a bulk load empties the per-cell and
857
+ per-row style maps but keeps the column / table layers."""
858
+ widget._state.pop("_cell_styles", None)
859
+ widget._state.pop("_row_styles", None)
860
+
861
+
862
+ def _add_tree_model_methods(attrs, all_methods):
863
+ """Override the tree/table mutators so they maintain the Python-side
864
+ model (see :mod:`pgwidgets.tree_model`).
865
+
866
+ Python is the source of truth: the browser can be rebuilt at any time
867
+ from ``_state``, so every mutation has to be reflected there or it is
868
+ lost on the next reconnect. The bulk setters additionally deep-copy,
869
+ so a caller that keeps editing the structure it passed in cannot
870
+ corrupt the model behind our back.
871
+ """
872
+ # --- bulk setters: store a private copy, reset row-level styles ---
873
+ for name, state_key in (("set_tree", "tree"), ("set_data", "data"),
874
+ ("set_rows", "rows")):
875
+ if name not in all_methods:
876
+ continue
877
+
878
+ def make_bulk(mn, key, pn):
879
+ def method(self, *args, **kwargs):
880
+ args = _resolve_kwargs(mn, pn, args, kwargs)
881
+ self._state[key] = tree_model.copy_tree(
882
+ args[0] if args else None)
883
+ _drop_row_styles(self)
884
+ return self._call(mn, *args)
885
+ method.__name__ = mn
886
+ return method
887
+ attrs[name] = make_bulk(name, state_key, all_methods[name])
888
+
889
+ # --- per-cell writes fold into the model ---
890
+ if "set_cell" in all_methods:
891
+ param_names = all_methods["set_cell"]
892
+
893
+ def set_cell_method(self, *args, **kwargs):
894
+ args = _resolve_kwargs("set_cell", param_names, args, kwargs)
895
+ if len(args) >= 3:
896
+ where, col, value = args[0], args[1], args[2]
897
+ tree = self._state.get("tree")
898
+ if isinstance(tree, dict):
899
+ tree_model.set_cell(tree, where, col, value)
900
+ else:
901
+ rows = _model_rows(self)
902
+ if rows is not None:
903
+ tree_model.row_set_cell(
904
+ rows, self._state.get("columns"),
905
+ where, col, value)
906
+ return self._call("set_cell", *args)
907
+ set_cell_method.__name__ = "set_cell"
908
+ attrs["set_cell"] = set_cell_method
909
+
910
+ # --- structural tree edits ---
911
+ if "add_item" in all_methods:
912
+ param_names = all_methods["add_item"]
913
+
914
+ def add_item_method(self, *args, **kwargs):
915
+ args = _resolve_kwargs("add_item", param_names, args, kwargs)
916
+ tree = self._state.get("tree")
917
+ if isinstance(tree, dict) and len(args) >= 3:
918
+ tree_model.add_item(tree, args[0], args[1], args[2])
919
+ return self._call("add_item", *args)
920
+ add_item_method.__name__ = "add_item"
921
+ attrs["add_item"] = add_item_method
922
+
923
+ if "remove_item" in all_methods:
924
+ param_names = all_methods["remove_item"]
925
+
926
+ def remove_item_method(self, *args, **kwargs):
927
+ args = _resolve_kwargs("remove_item", param_names, args, kwargs)
928
+ tree = self._state.get("tree")
929
+ if isinstance(tree, dict) and args:
930
+ tree_model.remove_item(tree, args[0])
931
+ return self._call("remove_item", *args)
932
+ remove_item_method.__name__ = "remove_item"
933
+ attrs["remove_item"] = remove_item_method
934
+
935
+ if "remove_items" in all_methods:
936
+ param_names = all_methods["remove_items"]
937
+
938
+ def remove_items_method(self, *args, **kwargs):
939
+ args = _resolve_kwargs("remove_items", param_names, args, kwargs)
940
+ tree = self._state.get("tree")
941
+ if isinstance(tree, dict) and args:
942
+ tree_model.remove_items(tree, args[0])
943
+ return self._call("remove_items", *args)
944
+ remove_items_method.__name__ = "remove_items"
945
+ attrs["remove_items"] = remove_items_method
946
+
947
+ if "add_tree" in all_methods:
948
+ param_names = all_methods["add_tree"]
949
+
950
+ def add_tree_method(self, *args, **kwargs):
951
+ args = _resolve_kwargs("add_tree", param_names, args, kwargs)
952
+ args = _pad(args, 2)
953
+ tree = self._state.get("tree")
954
+ if isinstance(tree, dict):
955
+ tree_model.merge_tree(tree, args[0], args[1])
956
+ elif isinstance(args[0], dict) and args[1] in (None, [], ()):
957
+ # merging into an empty widget: the merge *is* the tree
958
+ self._state["tree"] = tree_model.copy_tree(args[0])
959
+ return self._call("add_tree", *args)
960
+ add_tree_method.__name__ = "add_tree"
961
+ attrs["add_tree"] = add_tree_method
962
+
963
+ if "delete_tree" in all_methods:
964
+ param_names = all_methods["delete_tree"]
965
+
966
+ def delete_tree_method(self, *args, **kwargs):
967
+ args = _resolve_kwargs("delete_tree", param_names, args, kwargs)
968
+ args = _pad(args, 2)
969
+ tree = self._state.get("tree")
970
+ prune = True if args[1] is None else bool(args[1])
971
+ if isinstance(tree, dict):
972
+ tree_model.delete_tree(tree, args[0], prune)
973
+ return self._call("delete_tree", *args)
974
+ delete_tree_method.__name__ = "delete_tree"
975
+ attrs["delete_tree"] = delete_tree_method
976
+
977
+ if "update_tree" in all_methods:
978
+ param_names = all_methods["update_tree"]
979
+
980
+ def update_tree_method(self, *args, **kwargs):
981
+ """Bring the tree to `tree`, sending only what changed.
982
+
983
+ The browser's own ``update_tree`` is a full replacement (it
984
+ rebuilds every row, dropping expansion state, cell styles and
985
+ any open editor). Because the model here is authoritative we
986
+ can diff against it instead and send just the deltas. A diff
987
+ broader than a wholesale replacement falls back to
988
+ ``set_tree``.
989
+ """
990
+ args = _resolve_kwargs("update_tree", param_names, args, kwargs)
991
+ new_tree = args[0] if args else {}
992
+ old_tree = self._state.get("tree")
993
+
994
+ if not isinstance(old_tree, dict) or not isinstance(new_tree,
995
+ dict):
996
+ self._state["tree"] = tree_model.copy_tree(new_tree)
997
+ _drop_row_styles(self)
998
+ return self._call("set_tree", new_tree)
999
+
1000
+ ops = tree_model.diff_tree(old_tree, new_tree)
1001
+ self._state["tree"] = tree_model.copy_tree(new_tree)
1002
+ if ops is None:
1003
+ # wholesale replacement -- the browser clears, so the
1004
+ # row-level style layers go with it
1005
+ _drop_row_styles(self)
1006
+ return self._call("set_tree", new_tree)
1007
+ result = None
1008
+ for op in ops:
1009
+ result = self._call(op[0], *op[1:])
1010
+ return result
1011
+ update_tree_method.__name__ = "update_tree"
1012
+ attrs["update_tree"] = update_tree_method
1013
+
1014
+ # --- incremental flat-data update ---
1015
+ #
1016
+ # The browser diffs these against what it is showing, so the whole
1017
+ # array goes over the wire but only the rows that differ are
1018
+ # touched (selection, colours and scroll position survive). The
1019
+ # model still has to record the new contents for reconnection.
1020
+ for name, default_key in (("update_data", "data"),
1021
+ ("update_rows", "rows")):
1022
+ if name not in all_methods:
1023
+ continue
1024
+
1025
+ def make_update_rows(mn, dk, pn):
1026
+ def method(self, *args, **kwargs):
1027
+ args = _resolve_kwargs(mn, pn, args, kwargs)
1028
+ key = dk
1029
+ for candidate in ("rows", "data"):
1030
+ if isinstance(self._state.get(candidate), list):
1031
+ key = candidate # keep filling whichever
1032
+ break # bulk setter was used
1033
+ self._state[key] = tree_model.copy_tree(
1034
+ args[0] if args else None)
1035
+ return self._call(mn, *args)
1036
+ method.__name__ = mn
1037
+ return method
1038
+ attrs[name] = make_update_rows(name, default_key, all_methods[name])
1039
+
1040
+ # --- flat row edits ---
1041
+ for name, fn in (("insert_row", tree_model.insert_row),
1042
+ ("append_row", tree_model.append_row),
1043
+ ("delete_row", tree_model.delete_row)):
1044
+ if name not in all_methods:
1045
+ continue
1046
+
1047
+ def make_row_op(mn, func, pn):
1048
+ def method(self, *args, **kwargs):
1049
+ args = _resolve_kwargs(mn, pn, args, kwargs)
1050
+ rows = _model_rows(self)
1051
+ if rows is not None:
1052
+ if mn == "append_row":
1053
+ func(rows, args[0] if args else None)
1054
+ elif mn == "delete_row":
1055
+ func(rows, args[0] if args else None)
1056
+ else:
1057
+ args2 = _pad(args, 2)
1058
+ func(rows, args2[0], args2[1])
1059
+ return self._call(mn, *args)
1060
+ method.__name__ = mn
1061
+ return method
1062
+ attrs[name] = make_row_op(name, fn, all_methods[name])
1063
+
1064
+ # --- column edits ---
1065
+ if "insert_column" in all_methods:
1066
+ param_names = all_methods["insert_column"]
1067
+ # TreeView is insert_column(column, before); TableView is
1068
+ # insert_column(index, column) -- tell them apart by the defn
1069
+ by_key = param_names and param_names[0] == "column"
1070
+
1071
+ def insert_column_method(self, *args, **kwargs):
1072
+ args = _resolve_kwargs("insert_column", param_names,
1073
+ args, kwargs)
1074
+ args = _pad(args, 2)
1075
+ columns = self._state.get("columns")
1076
+ if isinstance(columns, list):
1077
+ if by_key:
1078
+ tree_model.insert_column(columns, args[0],
1079
+ before=args[1])
1080
+ else:
1081
+ tree_model.insert_column(columns, args[1],
1082
+ index=args[0])
1083
+ return self._call("insert_column", *args)
1084
+ insert_column_method.__name__ = "insert_column"
1085
+ attrs["insert_column"] = insert_column_method
1086
+
1087
+ if "append_column" in all_methods:
1088
+ param_names = all_methods["append_column"]
1089
+
1090
+ def append_column_method(self, *args, **kwargs):
1091
+ args = _resolve_kwargs("append_column", param_names,
1092
+ args, kwargs)
1093
+ columns = self._state.get("columns")
1094
+ if isinstance(columns, list) and args:
1095
+ tree_model.append_column(columns, args[0])
1096
+ return self._call("append_column", *args)
1097
+ append_column_method.__name__ = "append_column"
1098
+ attrs["append_column"] = append_column_method
1099
+
1100
+ if "delete_column" in all_methods:
1101
+ param_names = all_methods["delete_column"]
1102
+
1103
+ def delete_column_method(self, *args, **kwargs):
1104
+ args = _resolve_kwargs("delete_column", param_names,
1105
+ args, kwargs)
1106
+ columns = self._state.get("columns")
1107
+ if isinstance(columns, list) and args:
1108
+ # TreeView deletes by column key, TableView by index;
1109
+ # tree_model.delete_column accepts either
1110
+ tree_model.delete_column(columns, args[0])
1111
+ return self._call("delete_column", *args)
1112
+ delete_column_method.__name__ = "delete_column"
1113
+ attrs["delete_column"] = delete_column_method
1114
+
1115
+ _add_colour_override_methods(attrs, all_methods)
1116
+
1117
+
1118
+ def _record_style(widget, state_key, map_key, call_args, nkeys):
1119
+ """Record (or, when every colour channel is None, drop) one override.
1120
+
1121
+ ``call_args`` is the equivalent single-call argument tuple, so the
1122
+ stored entry can be replayed by re-dispatch regardless of whether it
1123
+ arrived singly or as part of a batch.
1124
+ """
1125
+ if all(v is None for v in call_args[nkeys:]):
1126
+ widget._state.get(state_key, {}).pop(map_key, None)
1127
+ else:
1128
+ widget._state.setdefault(state_key, {})[map_key] = call_args
1129
+
1130
+
1131
+ def _add_colour_override_methods(attrs, all_methods):
1132
+ """Accumulate the per-cell / row / column / table colour overrides.
1133
+
1134
+ These have no bulk setter to fold into, so they are kept as maps in
1135
+ _state and replayed on top of the data during reconstruction. An
1136
+ all-null call clears that layer, matching the JS.
1137
+ """
1138
+ # method, total args, state key, number of leading key args
1139
+ # (the remaining args are the fg / bg / bold channels)
1140
+ specs = [
1141
+ ("set_cell_color", 5, "_cell_styles", 2),
1142
+ ("set_row_color", 4, "_row_styles", 1),
1143
+ ("set_column_color", 4, "_column_styles", 1),
1144
+ ("set_table_color", 3, "_table_style", 0),
1145
+ ]
1146
+ for name, nargs, state_key, nkeys in specs:
1147
+ if name not in all_methods:
1148
+ continue
1149
+
1150
+ def make_setter(mn, n, sk, nk, pn):
1151
+ def method(self, *args, **kwargs):
1152
+ args = _resolve_kwargs(mn, pn, args, kwargs)
1153
+ args = _pad(args, n)
1154
+ cleared = all(v is None for v in args[nk:])
1155
+ if nk == 0: # table-wide: one slot
1156
+ if cleared:
1157
+ self._state.pop(sk, None)
1158
+ else:
1159
+ self._state[sk] = tuple(args)
1160
+ else:
1161
+ map_key = ((_style_path(args[0]), args[1]) if nk == 2
1162
+ else _style_path(args[0]))
1163
+ if cleared:
1164
+ self._state.get(sk, {}).pop(map_key, None)
1165
+ else:
1166
+ # keep the whole call, so replaying is a
1167
+ # straight re-dispatch
1168
+ self._state.setdefault(sk, {})[map_key] = tuple(args)
1169
+ return self._call(mn, *args)
1170
+ method.__name__ = mn
1171
+ return method
1172
+ attrs[name] = make_setter(name, nargs, state_key, nkeys,
1173
+ all_methods[name])
1174
+
1175
+ clears = [
1176
+ ("clear_cell_color", "_cell_styles",
1177
+ lambda a: (_style_path(a[0]), a[1])),
1178
+ ("clear_row_color", "_row_styles", lambda a: _style_path(a[0])),
1179
+ ("clear_column_color", "_column_styles", lambda a: a[0]),
1180
+ ]
1181
+ for name, state_key, keyfn in clears:
1182
+ if name not in all_methods:
1183
+ continue
1184
+
1185
+ def make_clear_one(mn, sk, kf, pn):
1186
+ def method(self, *args, **kwargs):
1187
+ args = _resolve_kwargs(mn, pn, args, kwargs)
1188
+ args = _pad(args, 2)
1189
+ self._state.get(sk, {}).pop(kf(args), None)
1190
+ return self._call(mn, *args)
1191
+ method.__name__ = mn
1192
+ return method
1193
+ attrs[name] = make_clear_one(name, state_key, keyfn,
1194
+ all_methods[name])
1195
+
1196
+ if "set_colors" in all_methods:
1197
+ param_names = all_methods["set_colors"]
1198
+
1199
+ def set_colors_method(self, *args, **kwargs):
1200
+ """Apply many colour overrides in one call.
1201
+
1202
+ Folds the batch into the same style maps the single-cell
1203
+ setters use, so the model stays accurate for reconnection
1204
+ while costing one round-trip and one browser re-render
1205
+ instead of one of each per cell.
1206
+ """
1207
+ args = _resolve_kwargs("set_colors", param_names, args, kwargs)
1208
+ spec = args[0] if args else None
1209
+ if isinstance(spec, dict):
1210
+ if spec.get("clear"):
1211
+ for key in ("_cell_styles", "_row_styles",
1212
+ "_column_styles", "_table_style"):
1213
+ self._state.pop(key, None)
1214
+ for e in (spec.get("cells") or []):
1215
+ _record_style(
1216
+ self, "_cell_styles",
1217
+ (_style_path(e.get("path")), e.get("col_key")),
1218
+ (e.get("path"), e.get("col_key"), e.get("fg"),
1219
+ e.get("bg"), e.get("bold")), 2)
1220
+ for e in (spec.get("rows") or []):
1221
+ _record_style(
1222
+ self, "_row_styles", _style_path(e.get("path")),
1223
+ (e.get("path"), e.get("fg"), e.get("bg"),
1224
+ e.get("bold")), 1)
1225
+ for e in (spec.get("columns") or []):
1226
+ _record_style(
1227
+ self, "_column_styles", e.get("col_key"),
1228
+ (e.get("col_key"), e.get("fg"), e.get("bg"),
1229
+ e.get("bold")), 1)
1230
+ if "table" in spec:
1231
+ t = spec.get("table") or {}
1232
+ channels = (t.get("fg"), t.get("bg"), t.get("bold"))
1233
+ if all(v is None for v in channels):
1234
+ self._state.pop("_table_style", None)
1235
+ else:
1236
+ self._state["_table_style"] = channels
1237
+ return self._call("set_colors", *args)
1238
+ set_colors_method.__name__ = "set_colors"
1239
+ attrs["set_colors"] = set_colors_method
1240
+
1241
+ if "clear_all_colors" in all_methods:
1242
+ def clear_all_colors_method(self):
1243
+ for key in ("_cell_styles", "_row_styles", "_column_styles",
1244
+ "_table_style"):
1245
+ self._state.pop(key, None)
1246
+ return self._call("clear_all_colors")
1247
+ clear_all_colors_method.__name__ = "clear_all_colors"
1248
+ attrs["clear_all_colors"] = clear_all_colors_method
1249
+
815
1250
 
816
1251
  def build_widget_class(js_class, defn):
817
1252
  """Build a synchronous stateful Widget subclass from a definition."""