pgwidgets-python 0.3.5__tar.gz → 0.4.0__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.
Files changed (80) hide show
  1. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/PKG-INFO +3 -4
  2. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/WhatsNew.rst +35 -0
  3. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/architecture.rst +45 -0
  4. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/async_/application.py +93 -2
  5. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/async_/widget.py +436 -2
  6. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/method_types.py +137 -7
  7. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/sync/application.py +118 -2
  8. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/sync/widget.py +437 -2
  9. pgwidgets_python-0.4.0/pgwidgets/tree_model.py +356 -0
  10. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets_python.egg-info/PKG-INFO +3 -4
  11. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets_python.egg-info/SOURCES.txt +4 -1
  12. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets_python.egg-info/requires.txt +1 -1
  13. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets_python.egg-info/scm_file_list.json +55 -52
  14. pgwidgets_python-0.4.0/pgwidgets_python.egg-info/scm_version.json +8 -0
  15. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pyproject.toml +11 -10
  16. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_session.py +59 -0
  17. pgwidgets_python-0.4.0/tests/test_tree_model.py +864 -0
  18. pgwidgets_python-0.4.0/tools/sync_async_tree_model.py +89 -0
  19. pgwidgets_python-0.3.5/pgwidgets_python.egg-info/scm_version.json +0 -8
  20. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/.flake8 +0 -0
  21. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/.github/workflows/tests.yml +0 -0
  22. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/.gitignore +0 -0
  23. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/.readthedocs.yaml +0 -0
  24. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/LICENSE.md +0 -0
  25. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/README.md +0 -0
  26. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/Makefile +0 -0
  27. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/api/async.rst +0 -0
  28. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/api/index.rst +0 -0
  29. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/api/sync.rst +0 -0
  30. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/async.rst +0 -0
  31. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/callbacks.rst +0 -0
  32. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/conf.py +0 -0
  33. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/extras.rst +0 -0
  34. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/getting-started.rst +0 -0
  35. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/index.rst +0 -0
  36. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/subclassing.rst +0 -0
  37. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/sync.rst +0 -0
  38. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/utilities.rst +0 -0
  39. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/web-servers.rst +0 -0
  40. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/docs/widgets.rst +0 -0
  41. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/README.md +0 -0
  42. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/all_widgets.py +0 -0
  43. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/all_widgets_async.py +0 -0
  44. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/demo_async.py +0 -0
  45. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/demo_sync.py +0 -0
  46. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/demo_treeview.py +0 -0
  47. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/flask-multi-process/README.md +0 -0
  48. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/flask-multi-process/gunicorn.conf.py +0 -0
  49. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/flask-multi-process/nginx.conf +0 -0
  50. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/flask-multi-process/server.py +0 -0
  51. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/examples/flask-multi-process/user_app.py +0 -0
  52. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/__init__.py +0 -0
  53. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/_json.py +0 -0
  54. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/async_/Widgets.py +0 -0
  55. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/async_/__init__.py +0 -0
  56. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/buffer.py +0 -0
  57. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/callbacks.py +0 -0
  58. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/defs.py +0 -0
  59. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/extras/__init__.py +0 -0
  60. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/extras/file_browser.py +0 -0
  61. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/icons/pgw-python-logo.png +0 -0
  62. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/sync/Widgets.py +0 -0
  63. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/sync/__init__.py +0 -0
  64. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/sync/text_source.py +0 -0
  65. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets/text_model.py +0 -0
  66. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
  67. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/pgwidgets_python.egg-info/top_level.txt +0 -0
  68. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/setup.cfg +0 -0
  69. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/test_pg2.py +0 -0
  70. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/__init__.py +0 -0
  71. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_async_session.py +0 -0
  72. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_defs.py +0 -0
  73. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_extras.py +0 -0
  74. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_fonts.py +0 -0
  75. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_protocol.py +0 -0
  76. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_reconstruct.py +0 -0
  77. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_resolve_kwargs.py +0 -0
  78. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_stateful.py +0 -0
  79. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_text_source.py +0 -0
  80. {pgwidgets_python-0.3.5 → pgwidgets_python-0.4.0}/tests/test_widget_classes.py +0 -0
@@ -1,15 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgwidgets-python
3
- Version: 0.3.5
3
+ Version: 0.4.0
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
@@ -17,7 +16,7 @@ Classifier: Topic :: Software Development :: User Interfaces
17
16
  Requires-Python: >=3.12
18
17
  Description-Content-Type: text/markdown
19
18
  License-File: LICENSE.md
20
- Requires-Dist: pgwidgets-js>=0.3.4
19
+ Requires-Dist: pgwidgets-js>=0.4.0
21
20
  Requires-Dist: websockets>=12
22
21
  Provides-Extra: dev
23
22
  Requires-Dist: sphinx; extra == "dev"
@@ -1,6 +1,41 @@
1
1
  What's New
2
2
  ==========
3
3
 
4
+ Recent changes — since ``v0.3.5``
5
+ ---------------------------------
6
+
7
+ TreeView / TableView: Python-authoritative model, batched updates
8
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9
+
10
+ Python is now the source of truth for the tree/table model, so a
11
+ browser reconnect rebuilds the *current* state instead of replaying
12
+ the original snapshot. Previously only the bulk setters were tracked,
13
+ so later mutations were fire-and-forget: a reconnect silently reverted
14
+ every change made since the last full load and dropped the colour
15
+ overrides entirely.
16
+
17
+ * All the incremental edits -- ``set_cell``, ``add_item`` /
18
+ ``remove_item`` / ``remove_items``, ``add_tree`` / ``delete_tree``,
19
+ and the row / column edits -- now apply to the stored model, so they
20
+ survive reconnect. Bulk setters deep-copy their input, so a caller
21
+ that keeps editing the structure it passed in can't corrupt the model.
22
+ * Colour overrides accumulate per cell / row / column / table and are
23
+ replayed after the data on reconnect, in chunked ``set_colors``
24
+ batches rather than one call per cell.
25
+ * ``update_tree`` diffs against the model and sends only the deltas, so
26
+ the browser keeps its expansion state, cell styles, and any open
27
+ editor.
28
+ * ``Session.batch()`` (and ``widget.batch()``) buffers a burst of calls
29
+ into a single ``batch`` message; runs of colour calls fold into
30
+ ``set_colors``, so a plain per-cell loop still redraws once. State
31
+ updates as each call is made, so a reconnect mid-batch still
32
+ reconstructs correctly, and a browser predating the batch message
33
+ falls back to individual calls.
34
+ * Fixed expand/collapse tracking after ``expand_all()`` /
35
+ ``collapse_all()`` (the ``"_all"`` sentinel raised ``AttributeError``
36
+ in the websocket handler when a node was later expanded or
37
+ collapsed).
38
+
4
39
  Recent changes — since ``v0.3.0``
5
40
  ---------------------------------
6
41
 
@@ -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.
@@ -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.add(key_path)
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: