pgwidgets-python 0.2.0__tar.gz → 0.2.3__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.2.0 → pgwidgets_python-0.2.3}/PKG-INFO +2 -2
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/WhatsNew.rst +75 -8
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/widgets.rst +55 -10
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/__init__.py +13 -0
- pgwidgets_python-0.2.3/pgwidgets/_json.py +63 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/async_/application.py +110 -23
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/async_/widget.py +48 -11
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/method_types.py +15 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/sync/application.py +115 -24
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/sync/widget.py +52 -11
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets_python.egg-info/PKG-INFO +2 -2
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets_python.egg-info/SOURCES.txt +1 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets_python.egg-info/requires.txt +1 -1
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pyproject.toml +5 -1
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/.flake8 +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/.github/workflows/tests.yml +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/.gitignore +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/.readthedocs.yaml +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/LICENSE.md +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/README.md +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/Makefile +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/api/async.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/api/index.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/api/sync.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/architecture.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/async.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/callbacks.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/conf.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/extras.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/getting-started.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/index.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/subclassing.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/sync.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/utilities.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/docs/web-servers.rst +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/examples/README.md +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/examples/all_widgets.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/examples/all_widgets_async.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/examples/demo_async.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/examples/demo_sync.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/examples/demo_treeview.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/async_/Widgets.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/async_/__init__.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/callbacks.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/defs.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/extras/__init__.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/extras/file_browser.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/sync/Widgets.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets/sync/__init__.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/pgwidgets_python.egg-info/top_level.txt +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/setup.cfg +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/test_pg2.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/tests/__init__.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/tests/test_defs.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/tests/test_extras.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/tests/test_protocol.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/tests/test_reconstruct.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/tests/test_session.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/tests/test_stateful.py +0 -0
- {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.3}/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.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Python bindings for the pgwidgets JavaScript widget library
|
|
5
5
|
Author: PGWidgets Developers
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -17,7 +17,7 @@ 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
|
|
20
|
+
Requires-Dist: pgwidgets-js>=0.2.0
|
|
21
21
|
Requires-Dist: websockets>=12
|
|
22
22
|
Provides-Extra: dev
|
|
23
23
|
Requires-Dist: sphinx; extra == "dev"
|
|
@@ -1,13 +1,76 @@
|
|
|
1
1
|
What's New
|
|
2
2
|
==========
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Recent changes — since ``v0.2.1``
|
|
5
|
+
---------------------------------
|
|
6
|
+
|
|
7
|
+
Reliable ``get_size()`` / ``get_position()`` on every visual widget
|
|
8
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
9
|
+
|
|
10
|
+
The auto-sync layer that backs widget getters has been reworked so
|
|
11
|
+
``widget.get_size()`` (and ``get_position()`` where supported)
|
|
12
|
+
returns the current layout-determined value for any visual widget,
|
|
13
|
+
not just ones that opted into the ``resizable`` / ``moveable``
|
|
14
|
+
options. The binding now installs a *passive* resize listener on
|
|
15
|
+
every visual widget — the value is captured into local state for
|
|
16
|
+
the getter, but it is **not** pushed to other connected browsers or
|
|
17
|
+
replayed on reconstruction. Explicit ``widget.resize(w, h)`` calls
|
|
18
|
+
are still replayed, as is interactive resize on widgets that opted
|
|
19
|
+
into active sync.
|
|
20
|
+
|
|
21
|
+
The user-visible upshot: code that calls ``image.get_size()`` on an
|
|
22
|
+
``Image`` placed in a flex container now returns the real pixel
|
|
23
|
+
size the browser laid the widget out at, instead of ``None`` or a
|
|
24
|
+
stale value. And widgets like ``Image`` with
|
|
25
|
+
``set_expanding(True, True)`` no longer get pinned to pixel
|
|
26
|
+
dimensions on reconnect (a regression in earlier auto-sync work).
|
|
27
|
+
|
|
28
|
+
The same guard now applies in both reconstruction paths
|
|
29
|
+
(top-level state replay AND ``_transfer_proxy`` for factory-created
|
|
30
|
+
widgets like ``ToolBarAction``), which fixes a "toolbar items
|
|
31
|
+
drift farther apart after each reconnect" bug.
|
|
32
|
+
|
|
33
|
+
``map`` callback survives reconstruction
|
|
34
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
35
|
+
|
|
36
|
+
The Python side now forwards ``map`` callbacks during a reconstruct
|
|
37
|
+
window instead of suppressing them along with the rest of the
|
|
38
|
+
state-replay echoes. ``map`` is a one-shot lifecycle event tied to
|
|
39
|
+
the widget first becoming visually present; missing it on the
|
|
40
|
+
Python side meant a user's map handler stayed un-fired until the
|
|
41
|
+
window happened to be resized. Pairs with the JS-side reliability
|
|
42
|
+
work for the same callback.
|
|
43
|
+
|
|
44
|
+
Sensible defaults for more getters
|
|
45
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
46
|
+
|
|
47
|
+
Getters now return useful zero-value defaults before any state has
|
|
48
|
+
been set or reported from the browser, instead of ``None``:
|
|
49
|
+
|
|
50
|
+
- ``get_scroll_position()`` → ``(0.0, 0.0)``
|
|
51
|
+
- ``get_scroll_percent()`` → ``(0.0, 0.0)`` (or ``0.0`` on
|
|
52
|
+
``ScrollBar``, which uses a single-axis API)
|
|
53
|
+
- ``get_thumb_percent()`` → ``(0.0, 0.0)`` (or ``0.0`` on
|
|
54
|
+
``ScrollBar``)
|
|
55
|
+
- ``get_expanding()`` → ``(False, False)``
|
|
56
|
+
- ``get_enabled()`` → ``True``
|
|
57
|
+
- ``get_state()`` → ``False``
|
|
58
|
+
- ``get_volume()`` → ``1.0`` (HTMLMediaElement convention: 0.0
|
|
59
|
+
muted, 1.0 full).
|
|
60
|
+
|
|
61
|
+
Configured in ``STATE_KEY_DEFAULTS`` / ``STATE_DEFAULTS`` in
|
|
62
|
+
``method_types.py``.
|
|
63
|
+
|
|
64
|
+
----
|
|
65
|
+
|
|
66
|
+
Earlier — since ``v0.1.3``
|
|
67
|
+
--------------------------
|
|
5
68
|
|
|
6
69
|
Major changes
|
|
7
|
-
|
|
70
|
+
~~~~~~~~~~~~~
|
|
8
71
|
|
|
9
72
|
TreeView / TableView: dict-tree model
|
|
10
|
-
|
|
73
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
11
74
|
|
|
12
75
|
Mirroring the JS-side rewrite, ``TreeView`` and ``TableView`` now
|
|
13
76
|
work with hierarchies of dicts keyed by stable string identifiers.
|
|
@@ -63,7 +126,7 @@ Highlights:
|
|
|
63
126
|
See :doc:`widgets` for the full reference.
|
|
64
127
|
|
|
65
128
|
Window controls (TopLevel) and shade (MDISubWindow)
|
|
66
|
-
|
|
129
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
67
130
|
|
|
68
131
|
``TopLevel`` gains the same window controls that ``MDISubWindow``
|
|
69
132
|
has, plus a "shade" (roll up to title bar) state on both.
|
|
@@ -96,7 +159,7 @@ Maximize, Close). The menu supports both click-release and
|
|
|
96
159
|
press-drag-release, like a menubar.
|
|
97
160
|
|
|
98
161
|
Image: binary-frame protocol
|
|
99
|
-
|
|
162
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
100
163
|
|
|
101
164
|
New method ``Image.set_binary_image(data, format='jpeg')`` sends raw
|
|
102
165
|
bytes (``bytes`` / ``bytearray`` / ``memoryview``) via a WebSocket
|
|
@@ -106,7 +169,7 @@ Useful for animation/streaming. ``format`` is one of ``"jpeg"``,
|
|
|
106
169
|
widget state and replayed on reconnect.
|
|
107
170
|
|
|
108
171
|
Callbacks base class
|
|
109
|
-
|
|
172
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
110
173
|
|
|
111
174
|
New module ``pgwidgets.callbacks`` exposes ``Callbacks``, a small
|
|
112
175
|
base class that provides the same callback API (``add_callback``,
|
|
@@ -121,7 +184,7 @@ supports both ``add_callback("activated", ...)`` and
|
|
|
121
184
|
See :ref:`callbacks-base`.
|
|
122
185
|
|
|
123
186
|
Robustness improvements
|
|
124
|
-
|
|
187
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
125
188
|
|
|
126
189
|
- ``Session._send`` and ``_send_binary`` no longer hang when the
|
|
127
190
|
asyncio loop refuses a coroutine (loop closed mid-call,
|
|
@@ -139,7 +202,7 @@ Robustness improvements
|
|
|
139
202
|
internal ``ScrollBar`` widgets in its constructor.
|
|
140
203
|
|
|
141
204
|
Other notable changes
|
|
142
|
-
|
|
205
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
143
206
|
|
|
144
207
|
- ``ColorDialog`` now exposes ``popup``, ``set_position``,
|
|
145
208
|
``set_modal``, and the ``move`` / ``close`` callbacks (inherited
|
|
@@ -160,3 +223,7 @@ Other notable changes
|
|
|
160
223
|
though the JS side preserves columns on clear).
|
|
161
224
|
- ``FileBrowser`` migrated to the new dict-tree ``TableView`` API
|
|
162
225
|
internally and now subclasses ``Callbacks``.
|
|
226
|
+
- ``FixedLayout`` container added (auto-generated from the JS
|
|
227
|
+
definition). Use ``W.FixedLayout()`` and
|
|
228
|
+
``layout.add_widget(child, x, y)`` to place children at fixed
|
|
229
|
+
pixel offsets; ``remove(child)`` works as on any other container.
|
|
@@ -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
|
-
``
|
|
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(
|
|
426
|
-
``
|
|
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
|
-
``
|
|
429
|
-
``
|
|
430
|
-
``
|
|
431
|
-
``
|
|
432
|
-
``
|
|
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
|
|
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
|
---------
|
|
@@ -16,4 +16,17 @@ Usage (asynchronous):
|
|
|
16
16
|
top = await W.TopLevel(title="Hello", resizable=True)
|
|
17
17
|
...
|
|
18
18
|
await app.run()
|
|
19
|
+
|
|
20
|
+
Version:
|
|
21
|
+
import pgwidgets
|
|
22
|
+
print(pgwidgets.__version__)
|
|
19
23
|
"""
|
|
24
|
+
|
|
25
|
+
from importlib.metadata import version as _pkg_version, PackageNotFoundError
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
__version__ = _pkg_version("pgwidgets")
|
|
29
|
+
except PackageNotFoundError:
|
|
30
|
+
# Package not installed (e.g. running from a source checkout
|
|
31
|
+
# without `pip install -e .`). Fall back to a sentinel.
|
|
32
|
+
__version__ = "0.0.0+unknown"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""JSON encoding helper for the remote protocol.
|
|
2
|
+
|
|
3
|
+
The wire format between Python and the browser is JSON. The stdlib
|
|
4
|
+
encoder has two issues for scientific data:
|
|
5
|
+
|
|
6
|
+
1. It rejects numpy scalars other than ``np.float64`` (which inherits
|
|
7
|
+
from float), numpy arrays, and similar buffer-protocol objects —
|
|
8
|
+
silently dropping a TreeView payload that happens to contain
|
|
9
|
+
``np.int64`` cell values.
|
|
10
|
+
|
|
11
|
+
2. It writes the literal tokens ``NaN`` / ``Infinity`` / ``-Infinity``
|
|
12
|
+
for non-finite floats, which browsers' ``JSON.parse`` reject — a
|
|
13
|
+
single masked / missing cell in a float column makes the whole
|
|
14
|
+
payload silently fail to parse on the JS side.
|
|
15
|
+
|
|
16
|
+
``JsonEncoder`` handles both: ``.item()`` / ``.tolist()`` fall-backs
|
|
17
|
+
for numpy/pandas types, and a pre-walk that replaces non-finite floats
|
|
18
|
+
with ``None`` (encoded as JSON ``null``). Use it via
|
|
19
|
+
``json.dumps(obj, cls=JsonEncoder)``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import math
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _scrub_nan(obj):
|
|
27
|
+
"""Recursively replace non-finite floats with None so the result
|
|
28
|
+
is RFC-8259-valid JSON when re-encoded. Cheap walk: O(n) and
|
|
29
|
+
only visits dicts/lists/tuples/floats."""
|
|
30
|
+
if isinstance(obj, float):
|
|
31
|
+
return obj if math.isfinite(obj) else None
|
|
32
|
+
if isinstance(obj, dict):
|
|
33
|
+
return {k: _scrub_nan(v) for k, v in obj.items()}
|
|
34
|
+
if isinstance(obj, (list, tuple)):
|
|
35
|
+
return [_scrub_nan(v) for v in obj]
|
|
36
|
+
return obj
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _coerce_scalar(v):
|
|
40
|
+
"""If *v* is a float, replace non-finite values with None."""
|
|
41
|
+
if isinstance(v, float) and not math.isfinite(v):
|
|
42
|
+
return None
|
|
43
|
+
return v
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class JsonEncoder(json.JSONEncoder):
|
|
47
|
+
def default(self, obj):
|
|
48
|
+
item = getattr(obj, "item", None)
|
|
49
|
+
if callable(item):
|
|
50
|
+
try:
|
|
51
|
+
return _coerce_scalar(item())
|
|
52
|
+
except (TypeError, ValueError):
|
|
53
|
+
pass
|
|
54
|
+
tolist = getattr(obj, "tolist", None)
|
|
55
|
+
if callable(tolist):
|
|
56
|
+
try:
|
|
57
|
+
return _scrub_nan(tolist())
|
|
58
|
+
except (TypeError, ValueError):
|
|
59
|
+
pass
|
|
60
|
+
return super().default(obj)
|
|
61
|
+
|
|
62
|
+
def iterencode(self, o, _one_shot=False):
|
|
63
|
+
return super().iterencode(_scrub_nan(o), _one_shot=_one_shot)
|
|
@@ -23,6 +23,7 @@ from pathlib import Path
|
|
|
23
23
|
import websockets
|
|
24
24
|
|
|
25
25
|
from pgwidgets_js import get_static_path, get_remote_html
|
|
26
|
+
from pgwidgets._json import JsonEncoder
|
|
26
27
|
from pgwidgets.defs import WIDGETS
|
|
27
28
|
from pgwidgets.method_types import (
|
|
28
29
|
SPECIAL_SETTERS, FIXED_SETTERS, CHILD_METHODS as CHILD_METHOD_NAMES,
|
|
@@ -90,6 +91,13 @@ class Session:
|
|
|
90
91
|
_STATE_KEY_TO_SETTER = {v: k for k, v in SPECIAL_SETTERS.items()}
|
|
91
92
|
# e.g. {"size": "resize"}
|
|
92
93
|
|
|
94
|
+
# Reverse map: state_key -> callback action that auto-syncs it
|
|
95
|
+
# (e.g. "size" -> "resize", "position" -> "move"). Used during
|
|
96
|
+
# state replay to skip keys that weren't actively opted into via
|
|
97
|
+
# _auto_sync_actions — those were captured passively for getter
|
|
98
|
+
# support but must not be replayed (would pin layout).
|
|
99
|
+
_STATE_KEY_TO_SYNC_ACTION = {v: k for k, v in STATE_SYNC_CALLBACKS.items()}
|
|
100
|
+
|
|
93
101
|
# State keys handled by fixed-value methods (show/hide)
|
|
94
102
|
_FIXED_STATE_KEYS = {}
|
|
95
103
|
for _mname, (_key, _val) in FIXED_SETTERS.items():
|
|
@@ -299,30 +307,46 @@ class Session:
|
|
|
299
307
|
|
|
300
308
|
def _dispatch_callback(self, wid, action, *args):
|
|
301
309
|
"""Dispatch a callback through the configured concurrency mode."""
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
# Suppress callbacks during reconstruction — they are side
|
|
311
|
+
# effects of state replay, not user actions. Exception: 'map'
|
|
312
|
+
# is a one-shot lifecycle event that the JS-side observers fire
|
|
313
|
+
# when a widget first gains a visible layout box; the timing
|
|
314
|
+
# can land inside the reconstruction window, and dropping it
|
|
315
|
+
# means the user's map handler never runs until a later layout
|
|
316
|
+
# change (e.g. window resize) triggers a re-fire.
|
|
317
|
+
if self._reconstructing and action != 'map':
|
|
318
|
+
return
|
|
304
319
|
|
|
305
320
|
# Auto-sync: some callbacks carry state that should be reflected
|
|
306
321
|
# in the Python-side widget (e.g. move -> position, resize -> size).
|
|
322
|
+
# We always *capture* the value so get_size()/get_position() return
|
|
323
|
+
# current values, but only *push* it to other browsers (and replay
|
|
324
|
+
# it on reconstruction) when the widget opted in via auto-sync.
|
|
325
|
+
# Otherwise a layout-determined size would replay as a literal
|
|
326
|
+
# resize(W, H) — pinning the widget to pixel dimensions and
|
|
327
|
+
# killing flex growth.
|
|
307
328
|
state_key = STATE_SYNC_CALLBACKS.get(action)
|
|
308
329
|
if state_key is not None:
|
|
309
330
|
widget = self._widget_map.get(wid)
|
|
310
331
|
if widget is not None:
|
|
332
|
+
auto = action in widget._auto_sync_actions
|
|
311
333
|
if len(args) == 1 and isinstance(args[0], dict):
|
|
312
334
|
d = args[0]
|
|
313
335
|
if "width" in d and "height" in d:
|
|
314
336
|
new_val = (d["width"], d["height"])
|
|
315
337
|
if widget._state.get(state_key) != new_val:
|
|
316
338
|
widget._state[state_key] = new_val
|
|
317
|
-
|
|
318
|
-
|
|
339
|
+
if auto:
|
|
340
|
+
self._push(wid, "resize",
|
|
341
|
+
d["width"], d["height"])
|
|
319
342
|
else:
|
|
320
343
|
new_val = tuple(args)
|
|
321
344
|
if widget._state.get(state_key) != new_val:
|
|
322
345
|
widget._state[state_key] = new_val
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
346
|
+
if auto:
|
|
347
|
+
setter = (self._STATE_KEY_TO_SETTER.get(state_key)
|
|
348
|
+
or f"set_{state_key}")
|
|
349
|
+
self._push(wid, setter, *args)
|
|
326
350
|
# If this widget wraps a child (e.g. MDISubWindow),
|
|
327
351
|
# propagate geometry into the parent's children options
|
|
328
352
|
# so reconstruction replays with the current pos/size.
|
|
@@ -492,7 +516,7 @@ class Session:
|
|
|
492
516
|
self._next_id += 1
|
|
493
517
|
msg["id"] = msg_id
|
|
494
518
|
try:
|
|
495
|
-
payload = json.dumps(msg)
|
|
519
|
+
payload = json.dumps(msg, cls=JsonEncoder)
|
|
496
520
|
except Exception as e:
|
|
497
521
|
self._logger.error(
|
|
498
522
|
f"JSON encode failed for "
|
|
@@ -510,7 +534,7 @@ class Session:
|
|
|
510
534
|
ff_id = self._next_id
|
|
511
535
|
self._next_id += 1
|
|
512
536
|
msg_copy = dict(msg, id=ff_id)
|
|
513
|
-
ff_payload = json.dumps(msg_copy)
|
|
537
|
+
ff_payload = json.dumps(msg_copy, cls=JsonEncoder)
|
|
514
538
|
for ws in self._connections[1:]:
|
|
515
539
|
_schedule_ws_send(ws, ff_payload)
|
|
516
540
|
result = await future
|
|
@@ -541,7 +565,7 @@ class Session:
|
|
|
541
565
|
"method": method,
|
|
542
566
|
"args": list(args),
|
|
543
567
|
"silent": True,
|
|
544
|
-
})
|
|
568
|
+
}, cls=JsonEncoder)
|
|
545
569
|
except Exception as e:
|
|
546
570
|
self._logger.error(
|
|
547
571
|
f"JSON encode failed for push {method} "
|
|
@@ -567,7 +591,7 @@ class Session:
|
|
|
567
591
|
"id": msg_id,
|
|
568
592
|
"wid": wid,
|
|
569
593
|
"action": action,
|
|
570
|
-
})
|
|
594
|
+
}, cls=JsonEncoder)
|
|
571
595
|
for ws in self._connections:
|
|
572
596
|
_schedule_ws_send(ws, payload)
|
|
573
597
|
|
|
@@ -630,7 +654,7 @@ class Session:
|
|
|
630
654
|
"wid": wid,
|
|
631
655
|
"method": method,
|
|
632
656
|
"args": list(args),
|
|
633
|
-
})
|
|
657
|
+
}, cls=JsonEncoder)
|
|
634
658
|
async def _pair(ws):
|
|
635
659
|
await ws.send(header)
|
|
636
660
|
await ws.send(data)
|
|
@@ -734,17 +758,37 @@ class Session:
|
|
|
734
758
|
cls = self._widget_classes.get(cls_name, Widget) if cls_name else Widget
|
|
735
759
|
widget = cls._from_existing(self, wid, cls_name or "Widget")
|
|
736
760
|
self._widget_map[wid] = widget
|
|
737
|
-
# Auto-listen for state-syncing callbacks (move, resize)
|
|
738
|
-
# so position/size changes are tracked for reconstruction.
|
|
761
|
+
# Auto-listen for state-syncing callbacks (move, resize).
|
|
739
762
|
# Register locally (sync) and send the listen message as
|
|
740
763
|
# true fire-and-forget (no result awaited) since
|
|
741
|
-
# _resolve_return is not async.
|
|
764
|
+
# _resolve_return is not async. For visual widgets we
|
|
765
|
+
# always listen for 'resize' so get_size() can return a
|
|
766
|
+
# current value, but we only mark the action as
|
|
767
|
+
# auto-syncing (which triggers push-to-peers and
|
|
768
|
+
# replay-on-reconstruction) when the widget defn opts in.
|
|
769
|
+
# Non-visual Callback-base objects (Timer, TextBufferRef,
|
|
770
|
+
# …) get nothing.
|
|
771
|
+
defn = WIDGETS.get(cls_name, {}) if cls_name else {}
|
|
772
|
+
opt_names_set = set(defn.get("options", []))
|
|
773
|
+
all_callbacks = defn.get("callbacks", [])
|
|
774
|
+
is_visual = defn.get("base") != "callback"
|
|
742
775
|
for action in STATE_SYNC_CALLBACKS:
|
|
743
|
-
|
|
744
|
-
if
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
776
|
+
req_opt = STATE_SYNC_REQUIRES_OPTION.get(action)
|
|
777
|
+
if req_opt is not None:
|
|
778
|
+
opted_in = req_opt in opt_names_set
|
|
779
|
+
else:
|
|
780
|
+
opted_in = action in all_callbacks
|
|
781
|
+
if not is_visual:
|
|
782
|
+
continue
|
|
783
|
+
if action == "resize" or opted_in:
|
|
784
|
+
key = f"{wid}:{action}"
|
|
785
|
+
if key not in self._callbacks:
|
|
786
|
+
self._callbacks[key] = [lambda wid, *a: None]
|
|
787
|
+
self._fire_and_forget_listen(wid, action)
|
|
788
|
+
if opted_in:
|
|
789
|
+
widget._auto_sync_actions.add(action)
|
|
790
|
+
elif action == "resize":
|
|
791
|
+
widget._passive_sync_actions.add(action)
|
|
748
792
|
return widget
|
|
749
793
|
if isinstance(val, list):
|
|
750
794
|
return [self._resolve_return(v) for v in val]
|
|
@@ -838,12 +882,23 @@ class Session:
|
|
|
838
882
|
await self._listen(new_widget._wid, act,
|
|
839
883
|
lambda wid, *a: None)
|
|
840
884
|
new_widget._auto_sync_actions.add(act)
|
|
841
|
-
# Replay any state the proxy accumulated (e.g. set_tooltip)
|
|
885
|
+
# Replay any state the proxy accumulated (e.g. set_tooltip).
|
|
886
|
+
# Skip passively-captured auto-sync state (size, position): we
|
|
887
|
+
# capture those from callbacks so getters work, but replaying
|
|
888
|
+
# would pin the widget to layout-determined pixel dimensions
|
|
889
|
+
# (same logic as _reconstruct_widget).
|
|
890
|
+
user_set = getattr(old_widget, "_user_set_state", set())
|
|
891
|
+
auto = getattr(old_widget, "_auto_sync_actions", set())
|
|
842
892
|
for key, value in old_widget._state.items():
|
|
843
893
|
if key.startswith("_"):
|
|
844
894
|
continue
|
|
845
895
|
if key in sync_keys:
|
|
846
896
|
continue
|
|
897
|
+
sync_action = self._STATE_KEY_TO_SYNC_ACTION.get(key)
|
|
898
|
+
if (sync_action is not None
|
|
899
|
+
and key not in user_set
|
|
900
|
+
and sync_action not in auto):
|
|
901
|
+
continue
|
|
847
902
|
method_name = (self._STATE_KEY_TO_SETTER.get(key)
|
|
848
903
|
or f"set_{key}")
|
|
849
904
|
if isinstance(value, tuple):
|
|
@@ -851,6 +906,13 @@ class Session:
|
|
|
851
906
|
else:
|
|
852
907
|
await self._call(new_widget._wid, method_name, value)
|
|
853
908
|
new_widget._state[key] = value
|
|
909
|
+
# Propagate user-set / auto-sync membership to the new
|
|
910
|
+
# widget so subsequent reconstructions replay consistently.
|
|
911
|
+
if sync_action is not None:
|
|
912
|
+
if key in user_set:
|
|
913
|
+
new_widget._user_set_state.add(key)
|
|
914
|
+
if sync_action in auto:
|
|
915
|
+
new_widget._auto_sync_actions.add(sync_action)
|
|
854
916
|
|
|
855
917
|
async def _ensure_reconstructed(self, widget):
|
|
856
918
|
"""Ensure a widget has been created on the JS side."""
|
|
@@ -1002,6 +1064,20 @@ class Session:
|
|
|
1002
1064
|
if key.startswith("_"):
|
|
1003
1065
|
continue
|
|
1004
1066
|
|
|
1067
|
+
# Skip auto-sync state (size, position) that came in
|
|
1068
|
+
# passively via a callback (e.g. layout-determined size).
|
|
1069
|
+
# We capture those so getters like get_size()/
|
|
1070
|
+
# get_position() work, but replaying them would pin the
|
|
1071
|
+
# widget to pixel dimensions and override flex/expanding
|
|
1072
|
+
# layout. Replay only if the user explicitly set the
|
|
1073
|
+
# value, or if the widget opted into the sync action
|
|
1074
|
+
# (e.g. an interactively-resizable widget).
|
|
1075
|
+
sync_action = self._STATE_KEY_TO_SYNC_ACTION.get(key)
|
|
1076
|
+
if (sync_action is not None
|
|
1077
|
+
and key not in widget._user_set_state
|
|
1078
|
+
and sync_action not in widget._auto_sync_actions):
|
|
1079
|
+
continue
|
|
1080
|
+
|
|
1005
1081
|
# Binary-payload state (e.g. set_binary_image) replays via
|
|
1006
1082
|
# _send_binary so the bytes go in a raw frame, not embedded
|
|
1007
1083
|
# as base64 in JSON.
|
|
@@ -1046,10 +1122,13 @@ class Session:
|
|
|
1046
1122
|
# _listen calls treat them as first-time registrations and
|
|
1047
1123
|
# actually send the "listen" message to the browser.
|
|
1048
1124
|
wid = widget._wid
|
|
1125
|
+
passive = getattr(widget, "_passive_sync_actions", set())
|
|
1049
1126
|
for action in list(saved_cbs.keys()):
|
|
1050
1127
|
self._callbacks.pop(f"{wid}:{action}", None)
|
|
1051
1128
|
for action in widget._auto_sync_actions:
|
|
1052
1129
|
self._callbacks.pop(f"{wid}:{action}", None)
|
|
1130
|
+
for action in passive:
|
|
1131
|
+
self._callbacks.pop(f"{wid}:{action}", None)
|
|
1053
1132
|
|
|
1054
1133
|
for action, entries in saved_cbs.items():
|
|
1055
1134
|
for handler, extra_args, extra_kwargs, style in entries:
|
|
@@ -1066,6 +1145,13 @@ class Session:
|
|
|
1066
1145
|
if action not in widget._registered_callbacks:
|
|
1067
1146
|
await self._listen(widget._wid, action,
|
|
1068
1147
|
lambda wid, *a: None)
|
|
1148
|
+
# Passive listeners (e.g. 'resize' for getter support on
|
|
1149
|
+
# widgets that didn't opt into auto-sync).
|
|
1150
|
+
for action in passive:
|
|
1151
|
+
if (action not in widget._registered_callbacks
|
|
1152
|
+
and action not in widget._auto_sync_actions):
|
|
1153
|
+
await self._listen(widget._wid, action,
|
|
1154
|
+
lambda wid, *a: None)
|
|
1069
1155
|
|
|
1070
1156
|
async def reconstruct(self):
|
|
1071
1157
|
"""Replay the entire widget tree to all connected browsers.
|
|
@@ -1332,7 +1418,8 @@ class Application:
|
|
|
1332
1418
|
async def _ws_handler(self, ws):
|
|
1333
1419
|
# Init handshake: send init, receive ack which may contain
|
|
1334
1420
|
# session_id + token for reconnection.
|
|
1335
|
-
await ws.send(json.dumps({"type": "init", "id": 0}
|
|
1421
|
+
await ws.send(json.dumps({"type": "init", "id": 0},
|
|
1422
|
+
cls=JsonEncoder))
|
|
1336
1423
|
ack_data = await ws.recv()
|
|
1337
1424
|
ack = json.loads(ack_data)
|
|
1338
1425
|
reconnect_sid = ack.get("session_id")
|
|
@@ -1399,7 +1486,7 @@ class Application:
|
|
|
1399
1486
|
"type": "session-info",
|
|
1400
1487
|
"session_id": session.id,
|
|
1401
1488
|
"token": session.token,
|
|
1402
|
-
}))
|
|
1489
|
+
}, cls=JsonEncoder))
|
|
1403
1490
|
|
|
1404
1491
|
if is_reconnect:
|
|
1405
1492
|
async def do_reconstruct():
|