pgwidgets-python 0.3.5__tar.gz → 0.4.1__tar.gz
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_python-0.3.5 → pgwidgets_python-0.4.1}/.github/workflows/tests.yml +1 -1
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/PKG-INFO +4 -4
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/WhatsNew.rst +59 -1
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/architecture.rst +45 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/sync.rst +2 -2
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/async_/application.py +93 -2
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/async_/widget.py +436 -2
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/method_types.py +137 -7
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/sync/application.py +118 -2
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/sync/text_source.py +46 -7
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/sync/widget.py +437 -2
- pgwidgets_python-0.4.1/pgwidgets/tree_model.py +356 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets_python.egg-info/PKG-INFO +4 -4
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets_python.egg-info/SOURCES.txt +4 -1
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets_python.egg-info/requires.txt +1 -1
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets_python.egg-info/scm_file_list.json +55 -52
- pgwidgets_python-0.4.1/pgwidgets_python.egg-info/scm_version.json +8 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pyproject.toml +13 -10
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_session.py +59 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_text_source.py +55 -0
- pgwidgets_python-0.4.1/tests/test_tree_model.py +864 -0
- pgwidgets_python-0.4.1/tools/sync_async_tree_model.py +89 -0
- pgwidgets_python-0.3.5/pgwidgets_python.egg-info/scm_version.json +0 -8
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/.flake8 +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/.gitignore +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/.readthedocs.yaml +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/LICENSE.md +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/README.md +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/Makefile +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/api/async.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/api/index.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/api/sync.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/async.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/callbacks.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/conf.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/extras.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/getting-started.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/index.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/subclassing.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/utilities.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/web-servers.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/docs/widgets.rst +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/README.md +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/all_widgets.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/all_widgets_async.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/demo_async.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/demo_sync.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/demo_treeview.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/flask-multi-process/README.md +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/flask-multi-process/gunicorn.conf.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/flask-multi-process/nginx.conf +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/flask-multi-process/server.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/examples/flask-multi-process/user_app.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/__init__.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/_json.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/async_/Widgets.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/async_/__init__.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/buffer.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/callbacks.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/defs.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/extras/__init__.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/extras/file_browser.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/icons/pgw-python-logo.png +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/sync/Widgets.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/sync/__init__.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets/text_model.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/pgwidgets_python.egg-info/top_level.txt +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/setup.cfg +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/test_pg2.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/__init__.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_async_session.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_defs.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_extras.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_fonts.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_protocol.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_reconstruct.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_resolve_kwargs.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_stateful.py +0 -0
- {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.1}/tests/test_widget_classes.py +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgwidgets-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Python bindings for the pgwidgets JavaScript widget library
|
|
5
5
|
Author: PGWidgets Developers
|
|
6
|
-
License: BSD-3-Clause
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
7
|
Project-URL: Homepage, https://github.com/naojsoft/pgwidgets-python
|
|
8
8
|
Project-URL: Repository, https://github.com/naojsoft/pgwidgets-python
|
|
9
9
|
Keywords: widgets,ui,gui,websocket,browser
|
|
10
10
|
Classifier: Development Status :: 3 - Alpha
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
13
12
|
Classifier: Programming Language :: Python :: 3
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
17
17
|
Requires-Python: >=3.12
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE.md
|
|
20
|
-
Requires-Dist: pgwidgets-js>=0.
|
|
20
|
+
Requires-Dist: pgwidgets-js>=0.4.1
|
|
21
21
|
Requires-Dist: websockets>=12
|
|
22
22
|
Provides-Extra: dev
|
|
23
23
|
Requires-Dist: sphinx; extra == "dev"
|
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
What's New
|
|
2
2
|
==========
|
|
3
3
|
|
|
4
|
+
Recent changes — since ``v0.4.0``
|
|
5
|
+
---------------------------------
|
|
6
|
+
|
|
7
|
+
TextSource: scroll alignment and caret styling
|
|
8
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
9
|
+
|
|
10
|
+
``scroll_to_ref(ref, align)`` and ``scroll_to_lineno(lineno, align)``
|
|
11
|
+
take an alignment -- ``'nearest'`` (the default, as before),
|
|
12
|
+
``'center'`` or ``'top'`` -- and pass it to the browser alongside the
|
|
13
|
+
offset. A search that scrolls to its match can now put the match in
|
|
14
|
+
the middle of the view rather than merely bringing it into sight.
|
|
15
|
+
|
|
16
|
+
``set_cursor_style(style, color)`` picks a ``'line'`` or ``'block'``
|
|
17
|
+
caret in any colour, and ``get_cursor_style()`` reads the pair back.
|
|
18
|
+
The choice is part of the model, replayed from
|
|
19
|
+
``_reconstruct_model()``, so a browser that (re)connects gets the caret
|
|
20
|
+
along with everything else. It is styled through the browser's own
|
|
21
|
+
``caret-color`` / ``caret-shape``, so it shows only while the editor
|
|
22
|
+
has the focus.
|
|
23
|
+
|
|
24
|
+
These need the matching methods in pgwidgets-js 0.4.1, which the
|
|
25
|
+
dependency floor now requires.
|
|
26
|
+
|
|
27
|
+
Recent changes — since ``v0.3.5``
|
|
28
|
+
---------------------------------
|
|
29
|
+
|
|
30
|
+
TreeView / TableView: Python-authoritative model, batched updates
|
|
31
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
32
|
+
|
|
33
|
+
Python is now the source of truth for the tree/table model, so a
|
|
34
|
+
browser reconnect rebuilds the *current* state instead of replaying
|
|
35
|
+
the original snapshot. Previously only the bulk setters were tracked,
|
|
36
|
+
so later mutations were fire-and-forget: a reconnect silently reverted
|
|
37
|
+
every change made since the last full load and dropped the colour
|
|
38
|
+
overrides entirely.
|
|
39
|
+
|
|
40
|
+
* All the incremental edits -- ``set_cell``, ``add_item`` /
|
|
41
|
+
``remove_item`` / ``remove_items``, ``add_tree`` / ``delete_tree``,
|
|
42
|
+
and the row / column edits -- now apply to the stored model, so they
|
|
43
|
+
survive reconnect. Bulk setters deep-copy their input, so a caller
|
|
44
|
+
that keeps editing the structure it passed in can't corrupt the model.
|
|
45
|
+
* Colour overrides accumulate per cell / row / column / table and are
|
|
46
|
+
replayed after the data on reconnect, in chunked ``set_colors``
|
|
47
|
+
batches rather than one call per cell.
|
|
48
|
+
* ``update_tree`` diffs against the model and sends only the deltas, so
|
|
49
|
+
the browser keeps its expansion state, cell styles, and any open
|
|
50
|
+
editor.
|
|
51
|
+
* ``Session.batch()`` (and ``widget.batch()``) buffers a burst of calls
|
|
52
|
+
into a single ``batch`` message; runs of colour calls fold into
|
|
53
|
+
``set_colors``, so a plain per-cell loop still redraws once. State
|
|
54
|
+
updates as each call is made, so a reconnect mid-batch still
|
|
55
|
+
reconstructs correctly, and a browser predating the batch message
|
|
56
|
+
falls back to individual calls.
|
|
57
|
+
* Fixed expand/collapse tracking after ``expand_all()`` /
|
|
58
|
+
``collapse_all()`` (the ``"_all"`` sentinel raised ``AttributeError``
|
|
59
|
+
in the websocket handler when a node was later expanded or
|
|
60
|
+
collapsed).
|
|
61
|
+
|
|
4
62
|
Recent changes — since ``v0.3.0``
|
|
5
63
|
---------------------------------
|
|
6
64
|
|
|
@@ -121,7 +179,7 @@ the 4th argument, so handlers that double-click a file can see
|
|
|
121
179
|
which column they hit. Existing handlers (3-arg) keep working.
|
|
122
180
|
|
|
123
181
|
``_resolve_kwargs``: skipped-positional kwargs
|
|
124
|
-
|
|
182
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
125
183
|
|
|
126
184
|
The kwarg resolver used by every generated widget method now
|
|
127
185
|
allows a caller to skip a positional argument and supply the
|
|
@@ -54,6 +54,9 @@ All messages are JSON objects with a ``type`` field.
|
|
|
54
54
|
call a method on a widget.
|
|
55
55
|
- ``{"type": "call", ..., "silent": true}`` -- call a method without
|
|
56
56
|
triggering callbacks (used for cross-browser sync).
|
|
57
|
+
- ``{"type": "batch", "calls": [{"wid": 1, "method": "set_cell",
|
|
58
|
+
"args": [...]}, ...]}`` -- apply many calls as one message (see
|
|
59
|
+
`Batching Updates`_).
|
|
57
60
|
- ``{"type": "listen", "wid": 1, "action": "activated"}`` -- subscribe to a
|
|
58
61
|
callback.
|
|
59
62
|
- ``{"type": "unlisten", "wid": 1, "action": "activated"}`` -- unsubscribe.
|
|
@@ -180,3 +183,45 @@ passed as an argument to another widget's method (e.g., ``vbox.add_widget(btn,
|
|
|
180
183
|
0)``), the framework automatically converts the Python ``Widget`` object to a
|
|
181
184
|
``{"__wid__": N}`` reference on the wire, and converts it back on return
|
|
182
185
|
values.
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
Batching Updates
|
|
189
|
+
----------------
|
|
190
|
+
|
|
191
|
+
Every widget call is normally its own message, and blocks until the
|
|
192
|
+
browser replies. While the UI is being built that costs nothing --
|
|
193
|
+
with no browser attached the calls return immediately -- so the cost
|
|
194
|
+
only shows up when a connected browser is being updated in bulk: a
|
|
195
|
+
refresh rewriting a few hundred cells, say. Two things make that slow:
|
|
196
|
+
one round-trip per call, and widgets such as ``TreeView`` re-rendering
|
|
197
|
+
on each one, which also makes the update paint in visible stages.
|
|
198
|
+
|
|
199
|
+
``Session.batch()`` (also reachable as ``widget.batch()``, since
|
|
200
|
+
batching is per session) buffers the calls and sends them as a single
|
|
201
|
+
``batch`` message. The browser applies them with rendering suspended
|
|
202
|
+
on each widget it touches, so every widget draws once:
|
|
203
|
+
|
|
204
|
+
.. code-block:: python
|
|
205
|
+
|
|
206
|
+
with tree.batch():
|
|
207
|
+
for path, col_key, value in changes:
|
|
208
|
+
tree.set_cell(path, col_key, value)
|
|
209
|
+
status.set_text("updated")
|
|
210
|
+
|
|
211
|
+
Semantics worth knowing:
|
|
212
|
+
|
|
213
|
+
- **State is still updated immediately.** Only the traffic is
|
|
214
|
+
deferred, so the Python-side model stays correct throughout and a
|
|
215
|
+
reconnect landing mid-batch reconstructs from it correctly.
|
|
216
|
+
- **Batched calls return None.** Methods that genuinely need an answer
|
|
217
|
+
from the browser -- getters, and the browser-only queries -- flush the
|
|
218
|
+
pending batch and execute immediately, so ordering is preserved.
|
|
219
|
+
- **Nesting is allowed;** only the outermost block flushes.
|
|
220
|
+
- **The buffer is flushed even if the body raises.** Those calls have
|
|
221
|
+
already been applied to the model, so dropping them would leave the
|
|
222
|
+
browser diverged from it.
|
|
223
|
+
|
|
224
|
+
Reconnection does its own batching: the colour overrides a tree or
|
|
225
|
+
table carries are replayed as a few large ``set_colors`` calls rather
|
|
226
|
+
than one per cell, so restoring a heavily coloured table is not
|
|
227
|
+
proportional to the number of round-trips.
|
|
@@ -114,8 +114,8 @@ Bytes are served from ``/_pgwidgets/font/<id>`` by the built-in
|
|
|
114
114
|
HTTP server with ``Cache-Control: immutable``; behind Flask /
|
|
115
115
|
nginx the same route is served from the Python process.
|
|
116
116
|
|
|
117
|
-
See also the
|
|
118
|
-
|
|
117
|
+
See also the "Custom fonts" section of the pgwidgets-js
|
|
118
|
+
documentation for the browser side of this.
|
|
119
119
|
|
|
120
120
|
Opening a URL in the browser
|
|
121
121
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
@@ -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:
|