pgwidgets-python 0.2.1__tar.gz → 0.3.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 (66) hide show
  1. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/PKG-INFO +1 -1
  2. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/WhatsNew.rst +145 -8
  3. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/architecture.rst +3 -1
  4. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/callbacks.rst +19 -13
  5. pgwidgets_python-0.3.0/docs/utilities.rst +81 -0
  6. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/widgets.rst +61 -10
  7. pgwidgets_python-0.3.0/examples/flask-multi-process/README.md +72 -0
  8. pgwidgets_python-0.3.0/examples/flask-multi-process/server.py +203 -0
  9. pgwidgets_python-0.3.0/examples/flask-multi-process/user_app.py +230 -0
  10. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/__init__.py +2 -0
  11. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/async_/application.py +245 -43
  12. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/async_/widget.py +38 -6
  13. pgwidgets_python-0.3.0/pgwidgets/buffer.py +125 -0
  14. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/method_types.py +49 -2
  15. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/sync/application.py +281 -46
  16. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/sync/widget.py +42 -6
  17. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets_python.egg-info/PKG-INFO +1 -1
  18. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets_python.egg-info/SOURCES.txt +4 -0
  19. pgwidgets_python-0.2.1/docs/utilities.rst +0 -40
  20. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/.flake8 +0 -0
  21. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/.github/workflows/tests.yml +0 -0
  22. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/.gitignore +0 -0
  23. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/.readthedocs.yaml +0 -0
  24. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/LICENSE.md +0 -0
  25. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/README.md +0 -0
  26. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/Makefile +0 -0
  27. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/api/async.rst +0 -0
  28. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/api/index.rst +0 -0
  29. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/api/sync.rst +0 -0
  30. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/async.rst +0 -0
  31. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/conf.py +0 -0
  32. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/extras.rst +0 -0
  33. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/getting-started.rst +0 -0
  34. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/index.rst +0 -0
  35. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/subclassing.rst +0 -0
  36. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/sync.rst +0 -0
  37. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/web-servers.rst +0 -0
  38. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/README.md +0 -0
  39. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/all_widgets.py +0 -0
  40. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/all_widgets_async.py +0 -0
  41. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/demo_async.py +0 -0
  42. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/demo_sync.py +0 -0
  43. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/demo_treeview.py +0 -0
  44. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/_json.py +0 -0
  45. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/async_/Widgets.py +0 -0
  46. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/async_/__init__.py +0 -0
  47. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/callbacks.py +0 -0
  48. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/defs.py +0 -0
  49. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/extras/__init__.py +0 -0
  50. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/extras/file_browser.py +0 -0
  51. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/sync/Widgets.py +0 -0
  52. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/sync/__init__.py +0 -0
  53. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
  54. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets_python.egg-info/requires.txt +0 -0
  55. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets_python.egg-info/top_level.txt +0 -0
  56. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pyproject.toml +0 -0
  57. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/setup.cfg +0 -0
  58. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/test_pg2.py +0 -0
  59. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/__init__.py +0 -0
  60. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_defs.py +0 -0
  61. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_extras.py +0 -0
  62. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_protocol.py +0 -0
  63. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_reconstruct.py +0 -0
  64. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_session.py +0 -0
  65. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_stateful.py +0 -0
  66. {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_widget_classes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgwidgets-python
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Python bindings for the pgwidgets JavaScript widget library
5
5
  Author: PGWidgets Developers
6
6
  License: BSD-3-Clause
@@ -1,13 +1,146 @@
1
1
  What's New
2
2
  ==========
3
3
 
4
- Significant changes since the last tagged release (``v0.1.3``).
4
+ Recent changes since ``v0.2.3``
5
+ ---------------------------------
6
+
7
+ Chunked binary transport (both directions)
8
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9
+
10
+ ``Session`` gained ``_send_binary_chunked(wid, method, args, data,
11
+ chunk_size=512KB, shape=None, dtype=None)`` and a matching
12
+ incoming-binary path. Large payloads automatically use the
13
+ chunked transport — :meth:`Image.set_binary_image` (and the
14
+ reconstruction replay of binary state) auto-pick chunked above a
15
+ 1 MiB threshold. ``Session._handle_file_chunk`` was rewritten as
16
+ ``_handle_binary_chunk`` to consume the new binary-chunk
17
+ envelope: chunks store by ``chunk_index`` (robust to ordering)
18
+ and reassemble as ``bytes``.
19
+
20
+ **Pre-1.0 API break**: drop-end / FileDialog ``activated``
21
+ handlers now receive each file's ``data`` field as raw ``bytes``
22
+ (was a ``"data:<mime>;base64,…"`` string). Each file dict now
23
+ also carries an ``encoding`` field (currently ``"bytes"``;
24
+ reserved for future ``"base64"``).
25
+
26
+ :class:`pgwidgets.Buffer` for typed-array delivery
27
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
28
+
29
+ New :class:`pgwidgets.Buffer` wraps raw bytes with ``shape`` and
30
+ ``dtype`` metadata. When passed to a binary-aware method, the
31
+ bytes ride the chunked transport and the receiver on the JS side
32
+ gets a typed array of the right shape (``Uint8Array``,
33
+ ``Float32Array``, …) without the per-method casting that used to
34
+ be necessary::
35
+
36
+ from pgwidgets import Buffer
37
+ pixels = ... # 2048 * 2048 * 4 bytes
38
+ viewer.load_buffer(
39
+ Buffer(pixels, shape=(2048, 2048, 4), dtype="uint8"),
40
+ [2048, 2048], cache)
41
+
42
+ Supported dtypes: ``uint8`` / ``uint16`` / ``uint32`` / ``int8``
43
+ / ``int16`` / ``int32`` / ``float32`` / ``float64``.
44
+ Construction validates that ``len(data) == prod(shape) *
45
+ itemsize``.
46
+
47
+ ``Application(ws_sock=…)`` for race-free port allocation
48
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
49
+
50
+ :class:`Application` accepts an optional pre-bound TCP socket via
51
+ ``ws_sock=``. When provided, ``Application`` adopts the socket
52
+ directly (hands it to ``websockets.serve(sock=...)``) instead of
53
+ binding ``(host, ws_port)`` itself, and reads ``ws_port`` back
54
+ from the socket for logging. The motivation is TOCTOU-free port
55
+ allocation: the caller can bind in advance, hand the socket in,
56
+ and never release the port between "find" and "use" — no other
57
+ process can grab it in the gap. The existing ``ws_port=`` API
58
+ is unchanged.
59
+
60
+ Flask multi-process demo
61
+ ~~~~~~~~~~~~~~~~~~~~~~~~
62
+
63
+ New ``examples/flask-multi-process/`` example: one pgwidgets
64
+ ``Application`` per visitor, each in its own OS process with its
65
+ own WebSocket port. Demonstrates the new ``ws_sock=`` parameter
66
+ (child binds, never releases), idle reaping via a grace timer on
67
+ ``app.on_disconnect`` that handles the subtle case where
68
+ ``app.on_connect`` doesn't fire on session-reconnect (the wrapper
69
+ also hooks ``session.add_connection``), and serving pgwidgets-js's
70
+ static assets from the pip-installed package rather than a CDN.
71
+
72
+ ----
73
+
74
+ Recent changes — since ``v0.2.1``
75
+ ---------------------------------
76
+
77
+ Reliable ``get_size()`` / ``get_position()`` on every visual widget
78
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
79
+
80
+ The auto-sync layer that backs widget getters has been reworked so
81
+ ``widget.get_size()`` (and ``get_position()`` where supported)
82
+ returns the current layout-determined value for any visual widget,
83
+ not just ones that opted into the ``resizable`` / ``moveable``
84
+ options. The binding now installs a *passive* resize listener on
85
+ every visual widget — the value is captured into local state for
86
+ the getter, but it is **not** pushed to other connected browsers or
87
+ replayed on reconstruction. Explicit ``widget.resize(w, h)`` calls
88
+ are still replayed, as is interactive resize on widgets that opted
89
+ into active sync.
90
+
91
+ The user-visible upshot: code that calls ``image.get_size()`` on an
92
+ ``Image`` placed in a flex container now returns the real pixel
93
+ size the browser laid the widget out at, instead of ``None`` or a
94
+ stale value. And widgets like ``Image`` with
95
+ ``set_expanding(True, True)`` no longer get pinned to pixel
96
+ dimensions on reconnect (a regression in earlier auto-sync work).
97
+
98
+ The same guard now applies in both reconstruction paths
99
+ (top-level state replay AND ``_transfer_proxy`` for factory-created
100
+ widgets like ``ToolBarAction``), which fixes a "toolbar items
101
+ drift farther apart after each reconnect" bug.
102
+
103
+ ``map`` callback survives reconstruction
104
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
105
+
106
+ The Python side now forwards ``map`` callbacks during a reconstruct
107
+ window instead of suppressing them along with the rest of the
108
+ state-replay echoes. ``map`` is a one-shot lifecycle event tied to
109
+ the widget first becoming visually present; missing it on the
110
+ Python side meant a user's map handler stayed un-fired until the
111
+ window happened to be resized. Pairs with the JS-side reliability
112
+ work for the same callback.
113
+
114
+ Sensible defaults for more getters
115
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
116
+
117
+ Getters now return useful zero-value defaults before any state has
118
+ been set or reported from the browser, instead of ``None``:
119
+
120
+ - ``get_scroll_position()`` → ``(0.0, 0.0)``
121
+ - ``get_scroll_percent()`` → ``(0.0, 0.0)`` (or ``0.0`` on
122
+ ``ScrollBar``, which uses a single-axis API)
123
+ - ``get_thumb_percent()`` → ``(0.0, 0.0)`` (or ``0.0`` on
124
+ ``ScrollBar``)
125
+ - ``get_expanding()`` → ``(False, False)``
126
+ - ``get_enabled()`` → ``True``
127
+ - ``get_state()`` → ``False``
128
+ - ``get_volume()`` → ``1.0`` (HTMLMediaElement convention: 0.0
129
+ muted, 1.0 full).
130
+
131
+ Configured in ``STATE_KEY_DEFAULTS`` / ``STATE_DEFAULTS`` in
132
+ ``method_types.py``.
133
+
134
+ ----
135
+
136
+ Earlier — since ``v0.1.3``
137
+ --------------------------
5
138
 
6
139
  Major changes
7
- -------------
140
+ ~~~~~~~~~~~~~
8
141
 
9
142
  TreeView / TableView: dict-tree model
10
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
143
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11
144
 
12
145
  Mirroring the JS-side rewrite, ``TreeView`` and ``TableView`` now
13
146
  work with hierarchies of dicts keyed by stable string identifiers.
@@ -63,7 +196,7 @@ Highlights:
63
196
  See :doc:`widgets` for the full reference.
64
197
 
65
198
  Window controls (TopLevel) and shade (MDISubWindow)
66
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
199
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
67
200
 
68
201
  ``TopLevel`` gains the same window controls that ``MDISubWindow``
69
202
  has, plus a "shade" (roll up to title bar) state on both.
@@ -96,7 +229,7 @@ Maximize, Close). The menu supports both click-release and
96
229
  press-drag-release, like a menubar.
97
230
 
98
231
  Image: binary-frame protocol
99
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
232
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
100
233
 
101
234
  New method ``Image.set_binary_image(data, format='jpeg')`` sends raw
102
235
  bytes (``bytes`` / ``bytearray`` / ``memoryview``) via a WebSocket
@@ -106,7 +239,7 @@ Useful for animation/streaming. ``format`` is one of ``"jpeg"``,
106
239
  widget state and replayed on reconnect.
107
240
 
108
241
  Callbacks base class
109
- ~~~~~~~~~~~~~~~~~~~~
242
+ ^^^^^^^^^^^^^^^^^^^^
110
243
 
111
244
  New module ``pgwidgets.callbacks`` exposes ``Callbacks``, a small
112
245
  base class that provides the same callback API (``add_callback``,
@@ -121,7 +254,7 @@ supports both ``add_callback("activated", ...)`` and
121
254
  See :ref:`callbacks-base`.
122
255
 
123
256
  Robustness improvements
124
- -----------------------
257
+ ~~~~~~~~~~~~~~~~~~~~~~~
125
258
 
126
259
  - ``Session._send`` and ``_send_binary`` no longer hang when the
127
260
  asyncio loop refuses a coroutine (loop closed mid-call,
@@ -139,7 +272,7 @@ Robustness improvements
139
272
  internal ``ScrollBar`` widgets in its constructor.
140
273
 
141
274
  Other notable changes
142
- ---------------------
275
+ ~~~~~~~~~~~~~~~~~~~~~
143
276
 
144
277
  - ``ColorDialog`` now exposes ``popup``, ``set_position``,
145
278
  ``set_modal``, and the ``move`` / ``close`` callbacks (inherited
@@ -160,3 +293,7 @@ Other notable changes
160
293
  though the JS side preserves columns on clear).
161
294
  - ``FileBrowser`` migrated to the new dict-tree ``TableView`` API
162
295
  internally and now subclasses ``Callbacks``.
296
+ - ``FixedLayout`` container added (auto-generated from the JS
297
+ definition). Use ``W.FixedLayout()`` and
298
+ ``layout.add_widget(child, x, y)`` to place children at fixed
299
+ pixel offsets; ``remove(child)`` works as on any other container.
@@ -68,7 +68,9 @@ All messages are JSON objects with a ``type`` field.
68
68
  - ``{"type": "error", "id": 1, "error": "..."}`` -- method error.
69
69
  - ``{"type": "callback", "wid": 1, "action": "activated", "args": [...]}`` --
70
70
  user interaction.
71
- - ``{"type": "file-chunk", ...}`` -- chunked file data (see :doc:`callbacks`).
71
+ - ``{"type": "binary-chunk", ...}`` -- one chunk of a chunked binary
72
+ transfer (used in both directions; file uploads, large image
73
+ payloads, etc.; see :doc:`callbacks`).
72
74
 
73
75
  Session Model
74
76
  -------------
@@ -127,9 +127,10 @@ The protocol works as follows:
127
127
  metadata (names, sizes, MIME types) but no file data.
128
128
  2. The framework fires a **drop-start** callback with the metadata so you
129
129
  can show progress UI.
130
- 3. The browser sends ``file-chunk`` messages with base64-encoded data.
130
+ 3. The browser sends ``binary-chunk`` JSON headers, each followed by a raw
131
+ binary WebSocket frame carrying the chunk's bytes (no base64 inflation).
131
132
  4. The framework fires **drop-progress** callbacks with transfer status.
132
- 5. When all chunks arrive, the framework reassembles the data and fires the
133
+ 5. When all chunks arrive, the framework reassembles the bytes and fires the
133
134
  **drop-end** callback with the complete payload.
134
135
 
135
136
  drop-start
@@ -175,22 +176,28 @@ drop-end
175
176
  ~~~~~~~~
176
177
 
177
178
  Fires when all file data has been received. The handler receives the full
178
- payload with base64 data URIs:
179
+ payload; each file dict carries:
180
+
181
+ - ``name`` (str) — original filename
182
+ - ``size`` (int) — byte size
183
+ - ``type`` (str) — MIME type from the browser (e.g. ``"image/png"``)
184
+ - ``encoding`` (str) — wire format of ``data``. Currently always
185
+ ``"bytes"`` (``data`` is raw bytes); reserved for ``"base64"`` if a
186
+ future sender chooses to deliver the file body without binary
187
+ reassembly.
188
+ - ``data`` (bytes or None) — file contents. ``None`` on read error;
189
+ in that case an additional ``error`` field carries the message.
179
190
 
180
191
  .. code-block:: python
181
192
 
182
- import base64
183
-
184
193
  def on_drop(payload):
185
194
  for f in payload["files"]:
195
+ if f["encoding"] != "bytes":
196
+ continue # currently only "bytes" is emitted
186
197
  name = f["name"]
187
198
  size = f["size"]
188
- mime = f["type"]
189
- data_uri = f["data"] # "data:<mime>;base64,<data>"
190
-
191
- # Decode the file content
192
- b64 = data_uri.split(",", 1)[1]
193
- content = base64.b64decode(b64)
199
+ mime = f["type"] # e.g. "image/png"
200
+ content = f["data"] # bytes
194
201
  print(f"Received {name}: {len(content)} bytes")
195
202
 
196
203
  widget.on("drop-end", on_drop)
@@ -210,8 +217,7 @@ Example: File Drop Zone
210
217
 
211
218
  def on_drop(payload):
212
219
  f = payload["files"][0]
213
- b64 = f["data"].split(",", 1)[1]
214
- text = base64.b64decode(b64).decode("utf-8", errors="replace")
220
+ text = f["data"].decode("utf-8", errors="replace")
215
221
  textarea.set_text(text)
216
222
  drop_label.set_text(f"Loaded: {f['name']}")
217
223
 
@@ -0,0 +1,81 @@
1
+ Utilities
2
+ =========
3
+
4
+ The ``Widget`` base class exposes a few static utility methods that are
5
+ useful when working with local files on the server side.
6
+
7
+ .. code-block:: python
8
+
9
+ from pgwidgets.sync.widget import Widget # or pgwidgets.async_.widget
10
+
11
+ to_data_uri
12
+ ------------
13
+
14
+ .. code-block:: python
15
+
16
+ Widget.to_data_uri(path)
17
+
18
+ Read a local file, base64-encode its contents, and return a
19
+ ``data:<mime>;base64,…`` string. The MIME type is guessed from the file
20
+ extension (falls back to ``application/octet-stream``).
21
+
22
+ The returned string can be passed directly to any widget method that
23
+ expects a URL, such as ``set_image()``, ``set_icon()``, or
24
+ ``add_cursor()``.
25
+
26
+ .. code-block:: python
27
+
28
+ # Load a local image into an Image widget
29
+ img.set_image(Widget.to_data_uri("/path/to/photo.png"))
30
+
31
+ # Set a button icon from a local file
32
+ btn.set_icon(Widget.to_data_uri("/path/to/icon.svg"))
33
+
34
+ .. note::
35
+
36
+ Methods listed in the ``_FILE_ARG_METHODS`` set (currently
37
+ ``set_icon``, ``set_image``, and ``set_icon_gutter``) already detect
38
+ local file paths and convert them automatically. Use
39
+ ``to_data_uri()`` explicitly when you need the data URI for other
40
+ purposes or want to cache/reuse it.
41
+
42
+
43
+ Buffer
44
+ ------
45
+
46
+ .. code-block:: python
47
+
48
+ from pgwidgets import Buffer
49
+
50
+ pixels = numpy_array.tobytes() # 2048 * 2048 * 4 bytes
51
+ buf = Buffer(pixels,
52
+ shape=(2048, 2048, 4),
53
+ dtype="uint8")
54
+ viewer.load_buffer(buf, [2048, 2048], cache)
55
+
56
+ A ``Buffer`` wraps raw ``bytes`` with ``shape`` and ``dtype``
57
+ metadata, so a method receiving the buffer on the JavaScript side
58
+ gets a properly-sized typed array (``Uint8Array``, ``Float32Array``,
59
+ …) instead of a raw ``ArrayBuffer``. Use it for image pixel data,
60
+ scientific arrays, vertex buffers — anything where the receiver
61
+ needs to know how to interpret the bytes without a hand-rolled
62
+ convention per method.
63
+
64
+ A ``Buffer`` always ships via the chunked binary transport
65
+ (``binary-call-chunked`` + ``binary-chunk`` messages), so multi-
66
+ megabyte payloads stream in 512 KiB chunks and don't block other
67
+ WebSocket traffic. ``shape`` and ``dtype`` ride on the announce
68
+ header — see :doc:`architecture`.
69
+
70
+ Supported dtypes:
71
+
72
+ - ``"uint8"`` / ``"uint16"`` / ``"uint32"``
73
+ - ``"int8"`` / ``"int16"`` / ``"int32"``
74
+ - ``"float32"`` / ``"float64"``
75
+
76
+ Methods that don't opt into ``Buffer`` keep receiving plain
77
+ ``bytes`` — :class:`Buffer` is purely additive. Construction
78
+ validates that the byte length equals
79
+ ``prod(shape) * dtype.itemsize``; pass ``shape=(len(data),)`` and
80
+ ``dtype="uint8"`` if you just want a flat byte buffer with a
81
+ known length.
@@ -140,6 +140,17 @@ Grid layout.
140
140
  ``append_column(widgets)``, ``delete_column(index)``
141
141
  - **Callbacks:** ``child-added``, ``child-removed``
142
142
 
143
+ FixedLayout
144
+ ~~~~~~~~~~~
145
+
146
+ Absolute-positioning container. Each child is placed at a fixed
147
+ ``(x, y)`` pixel offset within the container and renders at its
148
+ natural size unless ``resize()`` has been called on it.
149
+
150
+ - **Options:** *(none)*
151
+ - **Methods:** ``add_widget(child, x, y)``
152
+ - **Callbacks:** ``child-added``, ``child-removed``
153
+
143
154
  Splitter
144
155
  ~~~~~~~~
145
156
 
@@ -415,24 +426,58 @@ TextSource
415
426
 
416
427
  Source code editor with line numbers, syntax tags, and gutter icons.
417
428
 
429
+ Positions in the buffer are expressed as ``TextBufferRef`` instances —
430
+ live references that track edits. ``create_ref(offset, gravity)`` is
431
+ the only place a caller deals in raw integer offsets; all other
432
+ position-taking and position-returning methods on the public API use
433
+ refs.
434
+
435
+ .. note::
436
+
437
+ Refs are JS-side live objects. When using ``TextSource`` over the
438
+ remote interface (pgwidgets-python proper), refs cannot currently
439
+ round-trip across the wire — a ref-handle protocol is planned for
440
+ a future release. In the meantime, the ref-based API is fully
441
+ usable from JS-direct and pyodide contexts.
442
+
418
443
  - **Args:** ``text``
419
444
  - **Options:** ``wrap``, ``line_numbers``, ``icon_gutter``, ``editable``,
420
445
  ``font_family``, ``font_size``
421
446
  - **Methods:** ``set_text(text)``, ``get_text()``, ``get_length()``,
422
- ``insert_text(offset, text, tags)``, ``delete_range(start, end)``,
447
+ ``get_text_range(start_ref, end_ref)``,
448
+ ``insert_text(ref, text, tags)``, ``delete_range(start_ref, end_ref)``,
423
449
  ``clear()``, ``set_editable(tf)``, ``set_wrap(mode)``,
424
450
  ``set_line_numbers(tf)``, ``set_icon_gutter(tf)``,
425
- ``set_icon(line, icon_url)``, ``get_cursor()``, ``set_cursor(offset)``,
426
- ``get_selection()``, ``set_selection(start, end)``,
451
+ ``set_icon(ref, icon_url)``, ``get_cursor()``, ``set_cursor(ref)``,
452
+ ``get_selection_range()``, ``set_selection_range(start_ref, end_ref)``,
427
453
  ``create_tag(name, attrs)``, ``remove_tag_def(name)``,
428
- ``apply_tag(name, start, end)``, ``remove_tag(name, start, end)``,
429
- ``get_tags_at(offset)``, ``create_ref(offset, gravity)``,
430
- ``remove_ref(ref)``, ``undo()``, ``redo()``, ``can_undo()``,
431
- ``can_redo()``, ``find(query, opts)``, ``find_all(query, opts)``,
432
- ``replace(query, replacement, opts)``, ``scroll_to(ref_or_offset)``,
454
+ ``has_tag(name)``,
455
+ ``apply_tag(name, start_ref, end_ref)``,
456
+ ``remove_tag(name, start_ref, end_ref)``,
457
+ ``get_tags_at(ref)``, ``get_tags_range(start_ref, end_ref)``,
458
+ ``create_ref(offset, gravity)``, ``remove_ref(ref)``,
459
+ ``create_named_ref(name, offset, gravity)``,
460
+ ``get_named_ref(name)``, ``remove_named_ref(name)``,
461
+ ``get_ref_start()``, ``get_ref_end()``, ``get_ref_bounds()``,
462
+ ``get_ref_line_start(lineno)``, ``get_ref_line_end(lineno)``,
463
+ ``undo()``, ``redo()``, ``can_undo()``, ``can_redo()``,
464
+ ``find(query, opts)``, ``find_all(query, opts)``,
465
+ ``replace(query, replacement, opts)``, ``scroll_to_ref(ref)``,
433
466
  ``scroll_to_cursor()``
434
- - **Callbacks:** ``changed``, ``cursor_moved``, ``line_clicked``,
435
- ``icon_clicked``
467
+ - **Callbacks:** ``changed``, ``cursor_moved`` (fires with a fresh
468
+ ref), ``line_clicked``, ``icon_clicked`` (fires with
469
+ ``(line, ref)``)
470
+
471
+ A ``TextBufferRef`` itself supports the following methods:
472
+
473
+ - **Inspection:** ``get_offset()``, ``get_gravity()``, ``is_valid()``,
474
+ ``get_line()``, ``get_line_column()``
475
+ - **Absolute position:** ``set_offset(offset)``, ``set_line(lineno)``,
476
+ ``to_ref(other)``, ``copy()``
477
+ - **Relative movement:** ``to_line_start()``, ``to_line_end()``,
478
+ ``to_next_line()``, ``to_prev_line()``, ``to_next_char()``,
479
+ ``to_prev_char()`` (movement methods clamp at buffer boundaries
480
+ and are no-ops past them; mutating an invalidated ref raises)
436
481
 
437
482
  Selectors
438
483
  ---------
@@ -551,6 +596,12 @@ are pushed per second. ``format`` is one of ``'jpeg'``, ``'png'``,
551
596
  The latest frame is also stored in widget state so it is replayed
552
597
  on reconnect.
553
598
 
599
+ Payloads larger than ~1 MiB automatically use the *chunked* binary
600
+ transport (``binary-call-chunked`` + per-chunk ``binary-chunk``
601
+ messages) so the WebSocket can interleave control traffic while a
602
+ large frame streams. No API change for callers — pass ``bytes``
603
+ and the framework picks the right transport.
604
+
554
605
  Canvas
555
606
  ~~~~~~
556
607
 
@@ -0,0 +1,72 @@
1
+ # Flask multi-process pgwidgets demo
2
+
3
+ One pgwidgets `Application` per visitor, each in a fresh OS process
4
+ with its own WebSocket port.
5
+
6
+ - **`server.py`** — Flask front-end. Each request to `/` spawns a
7
+ child process running a fresh `Application`, waits for the child
8
+ to report its WebSocket port through a `multiprocessing.Queue`,
9
+ then returns an HTML page whose `RemoteInterface` connects back
10
+ to it.
11
+ - **`user_app.py`** — The per-visitor `Application` definition.
12
+ The child itself binds an ephemeral TCP socket, hands it directly
13
+ to `Application(ws_sock=...)` (so the port is held continuously —
14
+ no race with another process), and reports the resulting port
15
+ back through the queue. Edit the body of `setup(session)` to
16
+ build your UI. Can also be run standalone (`python user_app.py`)
17
+ for quick UI debugging without Flask in the loop; the port is
18
+ auto-allocated and logged.
19
+
20
+ ## Run
21
+
22
+ ```
23
+ pip install Flask pgwidgets-python pgwidgets-js
24
+ python server.py
25
+ # browse to http://localhost:5000/
26
+ ```
27
+
28
+ Every browser tab triggers a brand-new process. When the tab is
29
+ closed (or the browser navigates away), the child waits a grace
30
+ period — `IDLE_GRACE_SECONDS` in `user_app.py`, default 5 minutes —
31
+ to absorb page refreshes, brief network drops, and the user
32
+ closing/reopening a laptop, then exits. Reconnecting within the
33
+ grace window cancels the pending shutdown.
34
+
35
+ ### Listening on other interfaces
36
+
37
+ By default Flask and each per-visitor WebSocket bind to
38
+ ``127.0.0.1`` (loopback only). To accept connections from other
39
+ machines:
40
+
41
+ ```
42
+ python server.py --host 0.0.0.0 --port 8080
43
+ ```
44
+
45
+ The bind interface and the WebSocket URL are decoupled — the
46
+ browser is given a ``ws://`` URL derived from the same hostname it
47
+ used to reach Flask, so the same server can be reached via
48
+ ``localhost``, by IP address, or by DNS name with no further
49
+ configuration.
50
+
51
+ ## What the browser loads
52
+
53
+ Each HTML response embeds the per-visitor WebSocket URL. The
54
+ pgwidgets-js bundle (`Widgets.js`, `Widgets.css`, the `modules/`
55
+ subtree, icons) is served by Flask itself, from the pip-installed
56
+ `pgwidgets-js` Python package — `pgwidgets_js.get_static_path()`
57
+ returns the directory and a `/pgwidgets-js/<path>` route in
58
+ `server.py` exposes it. No CDN, no network dependency at runtime,
59
+ and the version on the wire always matches the one in your venv.
60
+
61
+ To point at an in-progress working copy of pgwidgets-js instead of
62
+ the installed package, either ``pip install -e .`` from the
63
+ pgwidgets-js source tree (so `get_static_path()` resolves to the
64
+ working copy), or replace `PGWIDGETS_JS_ROOT` in `server.py` with
65
+ a hard path to that tree's `static/` directory.
66
+
67
+ ## Caveats
68
+
69
+ - **Flask debug mode is off.** The reloader spawns a second
70
+ process, which combined with `multiprocessing` can produce
71
+ surprising behavior on Windows / macOS. Restart `server.py` by
72
+ hand after edits.