pgwidgets-python 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/PKG-INFO +2 -2
  2. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/__init__.py +13 -0
  3. pgwidgets_python-0.2.1/pgwidgets/_json.py +63 -0
  4. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/async_/application.py +9 -7
  5. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/async_/widget.py +10 -5
  6. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/sync/application.py +17 -6
  7. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/sync/widget.py +10 -5
  8. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets_python.egg-info/PKG-INFO +2 -2
  9. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets_python.egg-info/SOURCES.txt +1 -0
  10. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets_python.egg-info/requires.txt +1 -1
  11. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pyproject.toml +5 -1
  12. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/.flake8 +0 -0
  13. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/.github/workflows/tests.yml +0 -0
  14. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/.gitignore +0 -0
  15. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/.readthedocs.yaml +0 -0
  16. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/LICENSE.md +0 -0
  17. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/README.md +0 -0
  18. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/Makefile +0 -0
  19. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/WhatsNew.rst +0 -0
  20. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/api/async.rst +0 -0
  21. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/api/index.rst +0 -0
  22. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/api/sync.rst +0 -0
  23. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/architecture.rst +0 -0
  24. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/async.rst +0 -0
  25. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/callbacks.rst +0 -0
  26. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/conf.py +0 -0
  27. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/extras.rst +0 -0
  28. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/getting-started.rst +0 -0
  29. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/index.rst +0 -0
  30. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/subclassing.rst +0 -0
  31. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/sync.rst +0 -0
  32. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/utilities.rst +0 -0
  33. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/web-servers.rst +0 -0
  34. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/docs/widgets.rst +0 -0
  35. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/examples/README.md +0 -0
  36. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/examples/all_widgets.py +0 -0
  37. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/examples/all_widgets_async.py +0 -0
  38. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/examples/demo_async.py +0 -0
  39. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/examples/demo_sync.py +0 -0
  40. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/examples/demo_treeview.py +0 -0
  41. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/async_/Widgets.py +0 -0
  42. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/async_/__init__.py +0 -0
  43. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/callbacks.py +0 -0
  44. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/defs.py +0 -0
  45. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/extras/__init__.py +0 -0
  46. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/extras/file_browser.py +0 -0
  47. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/method_types.py +0 -0
  48. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/sync/Widgets.py +0 -0
  49. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets/sync/__init__.py +0 -0
  50. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
  51. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/pgwidgets_python.egg-info/top_level.txt +0 -0
  52. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/setup.cfg +0 -0
  53. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/test_pg2.py +0 -0
  54. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/tests/__init__.py +0 -0
  55. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/tests/test_defs.py +0 -0
  56. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/tests/test_extras.py +0 -0
  57. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/tests/test_protocol.py +0 -0
  58. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/tests/test_reconstruct.py +0 -0
  59. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/tests/test_session.py +0 -0
  60. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/tests/test_stateful.py +0 -0
  61. {pgwidgets_python-0.2.0 → pgwidgets_python-0.2.1}/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.0
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"
@@ -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,
@@ -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():
@@ -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
- if opt_names:
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
 
@@ -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
@@ -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
- if opt_names:
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.0
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"
@@ -30,6 +30,7 @@ examples/demo_async.py
30
30
  examples/demo_sync.py
31
31
  examples/demo_treeview.py
32
32
  pgwidgets/__init__.py
33
+ pgwidgets/_json.py
33
34
  pgwidgets/callbacks.py
34
35
  pgwidgets/defs.py
35
36
  pgwidgets/method_types.py
@@ -1,4 +1,4 @@
1
- pgwidgets-js
1
+ pgwidgets-js>=0.2.0
2
2
  websockets>=12
3
3
 
4
4
  [dev]
@@ -13,7 +13,11 @@ authors = [
13
13
  {name = "PGWidgets Developers"},
14
14
  ]
15
15
  dependencies = [
16
- "pgwidgets-js",
16
+ # Tests and runtime expect set_min_size/set_max_size in
17
+ # WIDGET_METHODS, the dict-tree TreeView shape, the binary
18
+ # Image protocol, and the new TopLevel window-control options
19
+ # — all introduced in pgwidgets-js 0.2.0.
20
+ "pgwidgets-js>=0.2.0",
17
21
  "websockets>=12",
18
22
  ]
19
23
  keywords = ["widgets", "ui", "gui", "websocket", "browser"]