pgwidgets-python 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pgwidgets/__init__.py +13 -0
- pgwidgets/_json.py +63 -0
- pgwidgets/async_/application.py +9 -7
- pgwidgets/async_/widget.py +10 -5
- pgwidgets/sync/application.py +17 -6
- pgwidgets/sync/widget.py +10 -5
- {pgwidgets_python-0.2.0.dist-info → pgwidgets_python-0.2.1.dist-info}/METADATA +2 -2
- {pgwidgets_python-0.2.0.dist-info → pgwidgets_python-0.2.1.dist-info}/RECORD +11 -10
- {pgwidgets_python-0.2.0.dist-info → pgwidgets_python-0.2.1.dist-info}/WHEEL +0 -0
- {pgwidgets_python-0.2.0.dist-info → pgwidgets_python-0.2.1.dist-info}/licenses/LICENSE.md +0 -0
- {pgwidgets_python-0.2.0.dist-info → pgwidgets_python-0.2.1.dist-info}/top_level.txt +0 -0
pgwidgets/__init__.py
CHANGED
|
@@ -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"
|
pgwidgets/_json.py
ADDED
|
@@ -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)
|
pgwidgets/async_/application.py
CHANGED
|
@@ -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,
|
|
@@ -492,7 +493,7 @@ class Session:
|
|
|
492
493
|
self._next_id += 1
|
|
493
494
|
msg["id"] = msg_id
|
|
494
495
|
try:
|
|
495
|
-
payload = json.dumps(msg)
|
|
496
|
+
payload = json.dumps(msg, cls=JsonEncoder)
|
|
496
497
|
except Exception as e:
|
|
497
498
|
self._logger.error(
|
|
498
499
|
f"JSON encode failed for "
|
|
@@ -510,7 +511,7 @@ class Session:
|
|
|
510
511
|
ff_id = self._next_id
|
|
511
512
|
self._next_id += 1
|
|
512
513
|
msg_copy = dict(msg, id=ff_id)
|
|
513
|
-
ff_payload = json.dumps(msg_copy)
|
|
514
|
+
ff_payload = json.dumps(msg_copy, cls=JsonEncoder)
|
|
514
515
|
for ws in self._connections[1:]:
|
|
515
516
|
_schedule_ws_send(ws, ff_payload)
|
|
516
517
|
result = await future
|
|
@@ -541,7 +542,7 @@ class Session:
|
|
|
541
542
|
"method": method,
|
|
542
543
|
"args": list(args),
|
|
543
544
|
"silent": True,
|
|
544
|
-
})
|
|
545
|
+
}, cls=JsonEncoder)
|
|
545
546
|
except Exception as e:
|
|
546
547
|
self._logger.error(
|
|
547
548
|
f"JSON encode failed for push {method} "
|
|
@@ -567,7 +568,7 @@ class Session:
|
|
|
567
568
|
"id": msg_id,
|
|
568
569
|
"wid": wid,
|
|
569
570
|
"action": action,
|
|
570
|
-
})
|
|
571
|
+
}, cls=JsonEncoder)
|
|
571
572
|
for ws in self._connections:
|
|
572
573
|
_schedule_ws_send(ws, payload)
|
|
573
574
|
|
|
@@ -630,7 +631,7 @@ class Session:
|
|
|
630
631
|
"wid": wid,
|
|
631
632
|
"method": method,
|
|
632
633
|
"args": list(args),
|
|
633
|
-
})
|
|
634
|
+
}, cls=JsonEncoder)
|
|
634
635
|
async def _pair(ws):
|
|
635
636
|
await ws.send(header)
|
|
636
637
|
await ws.send(data)
|
|
@@ -1332,7 +1333,8 @@ class Application:
|
|
|
1332
1333
|
async def _ws_handler(self, ws):
|
|
1333
1334
|
# Init handshake: send init, receive ack which may contain
|
|
1334
1335
|
# session_id + token for reconnection.
|
|
1335
|
-
await ws.send(json.dumps({"type": "init", "id": 0}
|
|
1336
|
+
await ws.send(json.dumps({"type": "init", "id": 0},
|
|
1337
|
+
cls=JsonEncoder))
|
|
1336
1338
|
ack_data = await ws.recv()
|
|
1337
1339
|
ack = json.loads(ack_data)
|
|
1338
1340
|
reconnect_sid = ack.get("session_id")
|
|
@@ -1399,7 +1401,7 @@ class Application:
|
|
|
1399
1401
|
"type": "session-info",
|
|
1400
1402
|
"session_id": session.id,
|
|
1401
1403
|
"token": session.token,
|
|
1402
|
-
}))
|
|
1404
|
+
}, cls=JsonEncoder))
|
|
1403
1405
|
|
|
1404
1406
|
if is_reconnect:
|
|
1405
1407
|
async def do_reconstruct():
|
pgwidgets/async_/widget.py
CHANGED
|
@@ -776,14 +776,19 @@ def _add_tree_view_methods(attrs, all_methods):
|
|
|
776
776
|
|
|
777
777
|
|
|
778
778
|
def _init_params(pos_names, opt_names):
|
|
779
|
-
"""Build the parameter string for the generated __init__.
|
|
779
|
+
"""Build the parameter string for the generated __init__.
|
|
780
|
+
|
|
781
|
+
Options are exposed as positional-or-keyword (no ``*`` separator)
|
|
782
|
+
so that single-option widgets like Frame accept ``Frame("Title")``
|
|
783
|
+
in addition to ``Frame(title="Title")``, matching the pyodide
|
|
784
|
+
bridge's behavior. Each option still defaults to ``None`` so
|
|
785
|
+
omitted ones don't override state from prior calls.
|
|
786
|
+
"""
|
|
780
787
|
params = ["self", "session"]
|
|
781
788
|
for name in pos_names:
|
|
782
789
|
params.append(f"{name}=None")
|
|
783
|
-
|
|
784
|
-
params.append("
|
|
785
|
-
for name in opt_names:
|
|
786
|
-
params.append(f"{name}=None")
|
|
790
|
+
for name in opt_names:
|
|
791
|
+
params.append(f"{name}=None")
|
|
787
792
|
params.append("**kwargs")
|
|
788
793
|
return ", ".join(params)
|
|
789
794
|
|
pgwidgets/sync/application.py
CHANGED
|
@@ -25,6 +25,7 @@ from pathlib import Path
|
|
|
25
25
|
import websockets
|
|
26
26
|
|
|
27
27
|
from pgwidgets_js import get_static_path, get_remote_html
|
|
28
|
+
from pgwidgets._json import JsonEncoder
|
|
28
29
|
from pgwidgets.defs import WIDGETS
|
|
29
30
|
from pgwidgets.sync.widget import Widget, build_all_widget_classes
|
|
30
31
|
from pgwidgets.method_types import (
|
|
@@ -260,6 +261,15 @@ class Session:
|
|
|
260
261
|
if threading.current_thread() is not self._cb_thread:
|
|
261
262
|
self._cb_thread.join(timeout=2)
|
|
262
263
|
|
|
264
|
+
def get_session_thread(self):
|
|
265
|
+
mode = self._app._concurrency
|
|
266
|
+
if mode == "serialized":
|
|
267
|
+
return threading.get_ident()
|
|
268
|
+
elif mode == "per_session":
|
|
269
|
+
return self._cb_thread
|
|
270
|
+
else:
|
|
271
|
+
return None
|
|
272
|
+
|
|
263
273
|
# -- Message handling --
|
|
264
274
|
|
|
265
275
|
def _handle_message(self, data):
|
|
@@ -541,7 +551,7 @@ class Session:
|
|
|
541
551
|
self._next_id += 1
|
|
542
552
|
msg["id"] = msg_id
|
|
543
553
|
try:
|
|
544
|
-
payload = json.dumps(msg)
|
|
554
|
+
payload = json.dumps(msg, cls=JsonEncoder)
|
|
545
555
|
except Exception as e:
|
|
546
556
|
self._logger.error(
|
|
547
557
|
f"JSON encode failed for "
|
|
@@ -565,7 +575,7 @@ class Session:
|
|
|
565
575
|
ff_id = self._next_id
|
|
566
576
|
self._next_id += 1
|
|
567
577
|
msg_copy = dict(msg, id=ff_id)
|
|
568
|
-
ff_payload = json.dumps(msg_copy)
|
|
578
|
+
ff_payload = json.dumps(msg_copy, cls=JsonEncoder)
|
|
569
579
|
for ws in self._connections[1:]:
|
|
570
580
|
_schedule_ws_send(self._app._loop, ws, ff_payload)
|
|
571
581
|
event.wait()
|
|
@@ -605,7 +615,7 @@ class Session:
|
|
|
605
615
|
"method": method,
|
|
606
616
|
"args": list(args),
|
|
607
617
|
"silent": True,
|
|
608
|
-
})
|
|
618
|
+
}, cls=JsonEncoder)
|
|
609
619
|
except Exception as e:
|
|
610
620
|
self._logger.error(
|
|
611
621
|
f"JSON encode failed for push {method} "
|
|
@@ -677,7 +687,7 @@ class Session:
|
|
|
677
687
|
"wid": wid,
|
|
678
688
|
"method": method,
|
|
679
689
|
"args": list(args),
|
|
680
|
-
})
|
|
690
|
+
}, cls=JsonEncoder)
|
|
681
691
|
# Send header + binary as an atomic pair on each connection so
|
|
682
692
|
# they can't be interleaved with other sends from another
|
|
683
693
|
# thread. The pair is wrapped in a single coroutine.
|
|
@@ -1512,7 +1522,8 @@ class Application:
|
|
|
1512
1522
|
async def _ws_handler(self, ws):
|
|
1513
1523
|
# Init handshake: send init, receive ack which may contain
|
|
1514
1524
|
# session_id + token for reconnection.
|
|
1515
|
-
await ws.send(json.dumps({"type": "init", "id": 0}
|
|
1525
|
+
await ws.send(json.dumps({"type": "init", "id": 0},
|
|
1526
|
+
cls=JsonEncoder))
|
|
1516
1527
|
ack_data = await ws.recv()
|
|
1517
1528
|
ack = json.loads(ack_data)
|
|
1518
1529
|
reconnect_sid = ack.get("session_id")
|
|
@@ -1582,7 +1593,7 @@ class Application:
|
|
|
1582
1593
|
"type": "session-info",
|
|
1583
1594
|
"session_id": session.id,
|
|
1584
1595
|
"token": session.token,
|
|
1585
|
-
}))
|
|
1596
|
+
}, cls=JsonEncoder))
|
|
1586
1597
|
|
|
1587
1598
|
if is_reconnect:
|
|
1588
1599
|
# Dispatch reconstruction to the session thread — calling
|
pgwidgets/sync/widget.py
CHANGED
|
@@ -842,14 +842,19 @@ def __init__({_init_params(pos_names, opt_names)}):
|
|
|
842
842
|
|
|
843
843
|
|
|
844
844
|
def _init_params(pos_names, opt_names):
|
|
845
|
-
"""Build the parameter string for the generated __init__.
|
|
845
|
+
"""Build the parameter string for the generated __init__.
|
|
846
|
+
|
|
847
|
+
Options are exposed as positional-or-keyword (no ``*`` separator)
|
|
848
|
+
so that single-option widgets like Frame accept ``Frame("Title")``
|
|
849
|
+
in addition to ``Frame(title="Title")``, matching the pyodide
|
|
850
|
+
bridge's behavior. Each option still defaults to ``None`` so
|
|
851
|
+
omitted ones don't override state from prior calls.
|
|
852
|
+
"""
|
|
846
853
|
params = ["self", "session"]
|
|
847
854
|
for name in pos_names:
|
|
848
855
|
params.append(f"{name}=None")
|
|
849
|
-
|
|
850
|
-
params.append("
|
|
851
|
-
for name in opt_names:
|
|
852
|
-
params.append(f"{name}=None")
|
|
856
|
+
for name in opt_names:
|
|
857
|
+
params.append(f"{name}=None")
|
|
853
858
|
params.append("**kwargs")
|
|
854
859
|
return ", ".join(params)
|
|
855
860
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgwidgets-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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,19 +1,20 @@
|
|
|
1
|
-
pgwidgets/__init__.py,sha256=
|
|
1
|
+
pgwidgets/__init__.py,sha256=fKKHAt9GxLbficihVyImWsqrnQvdQZQAmeGaRUF8cBE,857
|
|
2
|
+
pgwidgets/_json.py,sha256=o21qywJ6yAldbqxTq3nLwgK9O67r3a8JxhvI_WAJnMY,2184
|
|
2
3
|
pgwidgets/callbacks.py,sha256=gA2FnX0N5BmbmOMyEV35yHySgAfVjfAZcZhZbo7j4-c,3314
|
|
3
4
|
pgwidgets/defs.py,sha256=Q8qhvTeansFU1Av6j5ncdwF2xSNHrpXuKErdHWQ3HiA,436
|
|
4
5
|
pgwidgets/method_types.py,sha256=AoNXYY7iU-ttAWPqL104Dpl4AtUvyF6I2OmzpRw2HWQ,15637
|
|
5
6
|
pgwidgets/async_/Widgets.py,sha256=vld2xkvusBBEVbhbezaJsjBRRj3L7c17Up0pOVs8PGo,723
|
|
6
7
|
pgwidgets/async_/__init__.py,sha256=rXB-v9XRrYt1imYuYikhkzIyiRqaYhlTpQei2LHaQ18,564
|
|
7
|
-
pgwidgets/async_/application.py,sha256=
|
|
8
|
-
pgwidgets/async_/widget.py,sha256=
|
|
8
|
+
pgwidgets/async_/application.py,sha256=97jGnnwtrPHQYuh4BKsvl1yfWQnMe-fzIexvEiozw_o,64843
|
|
9
|
+
pgwidgets/async_/widget.py,sha256=y4tjgNunM3CUMlZIe00f07kL15w0zuu4whbcTRG8CGw,35871
|
|
9
10
|
pgwidgets/extras/__init__.py,sha256=AXUmFtnn4RSlp6pJX6z55j-m_29VmfzpYIjQvAy7pO0,325
|
|
10
11
|
pgwidgets/extras/file_browser.py,sha256=PWzJltjQZbmsD1ykbbC2xJ6ErEkJj7KafhmVkd9q9Ac,16963
|
|
11
12
|
pgwidgets/sync/Widgets.py,sha256=7SaocMVMzFHhi51pjuEAr47Wez90b_a61kDbiv0jOfM,706
|
|
12
13
|
pgwidgets/sync/__init__.py,sha256=SF5RTAvtu8BbYBWzpiCPipy6DJzNXf6nqk9xdvwxUhQ,542
|
|
13
|
-
pgwidgets/sync/application.py,sha256=
|
|
14
|
-
pgwidgets/sync/widget.py,sha256=
|
|
15
|
-
pgwidgets_python-0.2.
|
|
16
|
-
pgwidgets_python-0.2.
|
|
17
|
-
pgwidgets_python-0.2.
|
|
18
|
-
pgwidgets_python-0.2.
|
|
19
|
-
pgwidgets_python-0.2.
|
|
14
|
+
pgwidgets/sync/application.py,sha256=6x3zPcnDi2d2zNB7JzlLvr8lVAgbZNwsUj-ibcmxIGU,75823
|
|
15
|
+
pgwidgets/sync/widget.py,sha256=sMJVs4nrLg_8tTeJ3yqNh-bONGnwyhkzpn1TfYh0jAE,35937
|
|
16
|
+
pgwidgets_python-0.2.1.dist-info/licenses/LICENSE.md,sha256=LoM3fMTiMnQuHRCJghdjOtjnCrL8soBpu2PFk24Xvyg,1528
|
|
17
|
+
pgwidgets_python-0.2.1.dist-info/METADATA,sha256=QNcxLsgguIdFIAvn9mOS1YTL6Pq-V6uYRapohdYY7xE,4568
|
|
18
|
+
pgwidgets_python-0.2.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
19
|
+
pgwidgets_python-0.2.1.dist-info/top_level.txt,sha256=wwL6fBq0gU-JwzlM6TdduY1qYpu39ysqnnbQT-1bqAs,10
|
|
20
|
+
pgwidgets_python-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|