pgwidgets-python 0.3.5__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/async_/application.py +93 -2
- pgwidgets/async_/widget.py +436 -2
- pgwidgets/method_types.py +137 -7
- pgwidgets/sync/application.py +118 -2
- pgwidgets/sync/widget.py +437 -2
- pgwidgets/tree_model.py +356 -0
- {pgwidgets_python-0.3.5.dist-info → pgwidgets_python-0.4.0.dist-info}/METADATA +3 -4
- {pgwidgets_python-0.3.5.dist-info → pgwidgets_python-0.4.0.dist-info}/RECORD +11 -10
- {pgwidgets_python-0.3.5.dist-info → pgwidgets_python-0.4.0.dist-info}/WHEEL +1 -1
- {pgwidgets_python-0.3.5.dist-info → pgwidgets_python-0.4.0.dist-info}/licenses/LICENSE.md +0 -0
- {pgwidgets_python-0.3.5.dist-info → pgwidgets_python-0.4.0.dist-info}/top_level.txt +0 -0
pgwidgets/async_/application.py
CHANGED
|
@@ -11,6 +11,7 @@ the UI can be reconstructed when a browser reconnects.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
|
+
import contextlib
|
|
14
15
|
import json
|
|
15
16
|
import logging
|
|
16
17
|
import mimetypes
|
|
@@ -31,6 +32,8 @@ from pgwidgets.method_types import (
|
|
|
31
32
|
STATE_SYNC_CALLBACKS, STATE_SYNC_REQUIRES_OPTION,
|
|
32
33
|
WIDGET_CALLBACK_SYNC, POST_CHILDREN_STATE_KEYS, ITEM_LIST_CONFIG,
|
|
33
34
|
CHILD_CLOSE_CALLBACKS, REPLAY_METHODS, TREE_VIEW_WIDGETS,
|
|
35
|
+
TREE_OVERRIDE_REPLAY, colour_batches, JS_ONLY_METHODS,
|
|
36
|
+
coalesce_colour_calls,
|
|
34
37
|
BINARY_STATE_KEYS, _send_binary_auto,
|
|
35
38
|
)
|
|
36
39
|
from pgwidgets.async_.widget import Widget, build_all_widget_classes
|
|
@@ -142,6 +145,14 @@ class Session:
|
|
|
142
145
|
|
|
143
146
|
self._reconstructing = False # suppress callbacks during reconstruction
|
|
144
147
|
|
|
148
|
+
# Batching (see batch()). While _batch_depth is non-zero, calls
|
|
149
|
+
# are buffered here instead of being sent one at a time.
|
|
150
|
+
self._batch_depth = 0
|
|
151
|
+
self._batch_calls = []
|
|
152
|
+
# cleared if a browser rejects the 'batch' message; a new
|
|
153
|
+
# connection may be a newer client, so add_connection resets it
|
|
154
|
+
self._batch_supported = True
|
|
155
|
+
|
|
145
156
|
# Browser viewport size (updated by 'viewport' messages from JS).
|
|
146
157
|
self._screen_size = (0, 0)
|
|
147
158
|
|
|
@@ -198,6 +209,9 @@ class Session:
|
|
|
198
209
|
"""Add a browser connection to this session."""
|
|
199
210
|
if ws not in self._connections:
|
|
200
211
|
self._connections.append(ws)
|
|
212
|
+
# a reconnecting browser may be a newer client than the one
|
|
213
|
+
# that made us give up on batching
|
|
214
|
+
self._batch_supported = True
|
|
201
215
|
|
|
202
216
|
def remove_connection(self, ws):
|
|
203
217
|
"""Remove a browser connection from this session."""
|
|
@@ -459,7 +473,8 @@ class Session:
|
|
|
459
473
|
key_path = tuple(path) if isinstance(path, list) else path
|
|
460
474
|
expanded = widget._state.setdefault(
|
|
461
475
|
"_expanded_paths", set())
|
|
462
|
-
expanded
|
|
476
|
+
if expanded != "_all":
|
|
477
|
+
expanded.add(key_path)
|
|
463
478
|
collapsed = widget._state.get("_collapsed_paths")
|
|
464
479
|
if collapsed is not None and collapsed != "_all":
|
|
465
480
|
collapsed.discard(key_path)
|
|
@@ -468,7 +483,7 @@ class Session:
|
|
|
468
483
|
path = args[1]
|
|
469
484
|
key_path = tuple(path) if isinstance(path, list) else path
|
|
470
485
|
expanded = widget._state.get("_expanded_paths")
|
|
471
|
-
if expanded is not None:
|
|
486
|
+
if expanded is not None and expanded != "_all":
|
|
472
487
|
expanded.discard(key_path)
|
|
473
488
|
collapsed = widget._state.setdefault(
|
|
474
489
|
"_collapsed_paths", set())
|
|
@@ -678,11 +693,76 @@ class Session:
|
|
|
678
693
|
})
|
|
679
694
|
return wid
|
|
680
695
|
|
|
696
|
+
@contextlib.asynccontextmanager
|
|
697
|
+
async def batch(self):
|
|
698
|
+
"""Apply many updates as one message.
|
|
699
|
+
|
|
700
|
+
The async counterpart of the sync ``Session.batch``; see that
|
|
701
|
+
docstring for the semantics. Used as::
|
|
702
|
+
|
|
703
|
+
async with tree.batch():
|
|
704
|
+
for path, col, value in changes:
|
|
705
|
+
await tree.set_cell(path, col, value)
|
|
706
|
+
"""
|
|
707
|
+
self._batch_depth += 1
|
|
708
|
+
try:
|
|
709
|
+
yield self
|
|
710
|
+
finally:
|
|
711
|
+
self._batch_depth -= 1
|
|
712
|
+
if self._batch_depth == 0:
|
|
713
|
+
await self._flush_batch()
|
|
714
|
+
|
|
715
|
+
async def _flush_batch(self):
|
|
716
|
+
"""Send everything buffered by batch() as one message.
|
|
717
|
+
|
|
718
|
+
A browser older than this server won't know the ``batch``
|
|
719
|
+
message -- a page that was loaded before the server was
|
|
720
|
+
upgraded, most commonly. Rather than failing the caller's
|
|
721
|
+
update, fall back to sending the calls individually and stop
|
|
722
|
+
trying to batch for this connection. Reloading the page picks
|
|
723
|
+
up the current client and re-enables it.
|
|
724
|
+
"""
|
|
725
|
+
calls, self._batch_calls = self._batch_calls, []
|
|
726
|
+
if not calls:
|
|
727
|
+
return None
|
|
728
|
+
# _send handles the no-browser case (and is the single place
|
|
729
|
+
# that policy lives)
|
|
730
|
+
# Fold runs of colour calls into set_colors: one re-render in
|
|
731
|
+
# the browser instead of one per cell.
|
|
732
|
+
calls = coalesce_colour_calls(calls)
|
|
733
|
+
if self._batch_supported:
|
|
734
|
+
self._logger.debug("flushing a batch of %d call(s)", len(calls))
|
|
735
|
+
try:
|
|
736
|
+
return await self._send({"type": "batch", "calls": calls})
|
|
737
|
+
except RuntimeError as e:
|
|
738
|
+
if "Unknown message type" not in str(e):
|
|
739
|
+
raise
|
|
740
|
+
self._batch_supported = False
|
|
741
|
+
self._logger.warning(
|
|
742
|
+
"browser does not understand batched updates (it is "
|
|
743
|
+
"running an older pgwidgets-js); sending calls "
|
|
744
|
+
"individually. Reload the page to re-enable batching.")
|
|
745
|
+
for call in calls:
|
|
746
|
+
await self._send({"type": "call", **call})
|
|
747
|
+
return None
|
|
748
|
+
|
|
749
|
+
def _needs_result(self, method):
|
|
750
|
+
"""True if `method` has to reach the browser now to be useful."""
|
|
751
|
+
return method.startswith("get_") or method in JS_ONLY_METHODS
|
|
752
|
+
|
|
681
753
|
async def _call(self, wid, method, *args):
|
|
682
754
|
"""Call a method on a JS widget.
|
|
683
755
|
|
|
684
756
|
Returns None if no browser is connected.
|
|
685
757
|
"""
|
|
758
|
+
if self._batch_depth > 0:
|
|
759
|
+
if not self._needs_result(method):
|
|
760
|
+
self._batch_calls.append({"wid": wid, "method": method,
|
|
761
|
+
"args": list(args)})
|
|
762
|
+
return None
|
|
763
|
+
# has to go now -- flush what's queued so it stays ordered
|
|
764
|
+
await self._flush_batch()
|
|
765
|
+
|
|
686
766
|
result = await self._send({
|
|
687
767
|
"type": "call",
|
|
688
768
|
"wid": wid,
|
|
@@ -1335,6 +1415,13 @@ class Session:
|
|
|
1335
1415
|
list(path))
|
|
1336
1416
|
continue
|
|
1337
1417
|
|
|
1418
|
+
# Tree/table colour overrides are replayed together as
|
|
1419
|
+
# batches once the whole state has been walked (see
|
|
1420
|
+
# below) -- one round-trip and one browser re-render per
|
|
1421
|
+
# batch, rather than per coloured cell.
|
|
1422
|
+
if key in TREE_OVERRIDE_REPLAY or key == "_table_style":
|
|
1423
|
+
continue
|
|
1424
|
+
|
|
1338
1425
|
# Tree/table sort
|
|
1339
1426
|
if key == "_sort":
|
|
1340
1427
|
col, asc = value
|
|
@@ -1350,6 +1437,10 @@ class Session:
|
|
|
1350
1437
|
else:
|
|
1351
1438
|
await self._call(widget._wid, method_name, value)
|
|
1352
1439
|
|
|
1440
|
+
# Colour overrides, batched
|
|
1441
|
+
for spec in colour_batches(widget):
|
|
1442
|
+
await self._call(widget._wid, "set_colors", spec)
|
|
1443
|
+
|
|
1353
1444
|
# Show/hide
|
|
1354
1445
|
for key, value in widget._state.items():
|
|
1355
1446
|
if key in self._FIXED_STATE_KEYS:
|
pgwidgets/async_/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,
|
|
@@ -279,6 +280,15 @@ class Widget:
|
|
|
279
280
|
"""The Session this widget belongs to."""
|
|
280
281
|
return self._session
|
|
281
282
|
|
|
283
|
+
def batch(self):
|
|
284
|
+
"""Convenience for ``widget.session.batch()``.
|
|
285
|
+
|
|
286
|
+
Batching is per session, not per widget, so updates to other
|
|
287
|
+
widgets made inside the block ride along in the same message.
|
|
288
|
+
Used with ``async with``.
|
|
289
|
+
"""
|
|
290
|
+
return self._session.batch()
|
|
291
|
+
|
|
282
292
|
@property
|
|
283
293
|
def app(self):
|
|
284
294
|
"""The Application this widget belongs to."""
|
|
@@ -773,7 +783,8 @@ def _add_tree_view_methods(attrs, all_methods):
|
|
|
773
783
|
if path is not None:
|
|
774
784
|
key = tuple(path) if isinstance(path, list) else path
|
|
775
785
|
expanded = self._state.setdefault("_expanded_paths", set())
|
|
776
|
-
expanded
|
|
786
|
+
if expanded != "_all":
|
|
787
|
+
expanded.add(key)
|
|
777
788
|
collapsed = self._state.get("_collapsed_paths")
|
|
778
789
|
if collapsed is not None and collapsed != "_all":
|
|
779
790
|
collapsed.discard(key)
|
|
@@ -789,7 +800,7 @@ def _add_tree_view_methods(attrs, all_methods):
|
|
|
789
800
|
if path is not None:
|
|
790
801
|
key = tuple(path) if isinstance(path, list) else path
|
|
791
802
|
expanded = self._state.get("_expanded_paths")
|
|
792
|
-
if expanded is not None:
|
|
803
|
+
if expanded is not None and expanded != "_all":
|
|
793
804
|
expanded.discard(key)
|
|
794
805
|
collapsed = self._state.setdefault("_collapsed_paths", set())
|
|
795
806
|
if collapsed != "_all":
|
|
@@ -814,6 +825,429 @@ def _add_tree_view_methods(attrs, all_methods):
|
|
|
814
825
|
collapse_all_method.__name__ = "collapse_all"
|
|
815
826
|
attrs["collapse_all"] = collapse_all_method
|
|
816
827
|
|
|
828
|
+
_add_tree_model_methods(attrs, all_methods)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
# ---- generated from sync/widget.py by tools/sync_async_tree_model.py ----
|
|
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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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 await self._call("set_tree", new_tree)
|
|
1007
|
+
result = None
|
|
1008
|
+
for op in ops:
|
|
1009
|
+
result = await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await 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
|
+
async 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 await self._call("clear_all_colors")
|
|
1247
|
+
clear_all_colors_method.__name__ = "clear_all_colors"
|
|
1248
|
+
attrs["clear_all_colors"] = clear_all_colors_method
|
|
1249
|
+
|
|
1250
|
+
|
|
817
1251
|
|
|
818
1252
|
def _init_params(pos_names, opt_names):
|
|
819
1253
|
"""Build the parameter string for the generated __init__.
|